mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
gh-127111: Apply prettier formatter to Emscripten web example (#127551)
Cleaned up formatting (and a stray closing tag) of the web example HTML and JS.
This commit is contained in:
parent
94b8f8b409
commit
2f1cee8477
2 changed files with 490 additions and 425 deletions
|
@ -1,373 +1,433 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="author" content="Katie Bell">
|
<meta name="author" content="Katie Bell" />
|
||||||
<meta name="description" content="Simple REPL for Python WASM">
|
<meta name="description" content="Simple REPL for Python WASM" />
|
||||||
<title>wasm-python terminal</title>
|
<title>wasm-python terminal</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/xterm@4.18.0/css/xterm.css" crossorigin integrity="sha384-4eEEn/eZgVHkElpKAzzPx/Kow/dTSgFk1BNe+uHdjHa+NkZJDh5Vqkq31+y7Eycd"/>
|
<link
|
||||||
<style>
|
rel="stylesheet"
|
||||||
body {
|
href="https://unpkg.com/xterm@4.18.0/css/xterm.css"
|
||||||
font-family: arial;
|
crossorigin
|
||||||
max-width: 800px;
|
integrity="sha384-4eEEn/eZgVHkElpKAzzPx/Kow/dTSgFk1BNe+uHdjHa+NkZJDh5Vqkq31+y7Eycd"
|
||||||
margin: 0 auto
|
/>
|
||||||
}
|
<style>
|
||||||
#code {
|
body {
|
||||||
width: 100%;
|
font-family: arial;
|
||||||
height: 180px;
|
max-width: 800px;
|
||||||
}
|
margin: 0 auto;
|
||||||
#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 integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"/></script>
|
|
||||||
<script type="module">
|
|
||||||
class WorkerManager {
|
|
||||||
constructor(workerURL, standardIO, readyCallBack, finishedCallback) {
|
|
||||||
this.workerURL = workerURL
|
|
||||||
this.worker = null
|
|
||||||
this.standardIO = standardIO
|
|
||||||
this.readyCallBack = readyCallBack
|
|
||||||
this.finishedCallback = finishedCallback
|
|
||||||
|
|
||||||
this.initialiseWorker()
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialiseWorker() {
|
|
||||||
if (!this.worker) {
|
|
||||||
this.worker = new Worker(this.workerURL, {type: "module"})
|
|
||||||
this.worker.addEventListener('message', this.handleMessageFromWorker)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(options) {
|
|
||||||
this.worker.postMessage({
|
|
||||||
type: 'run',
|
|
||||||
args: options.args || [],
|
|
||||||
files: options.files || {}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
if (this.worker) {
|
|
||||||
this.worker.terminate()
|
|
||||||
this.worker = null
|
|
||||||
}
|
|
||||||
this.standardIO.message('Worker process terminated.')
|
|
||||||
this.initialiseWorker()
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
#code {
|
||||||
data.forEach((value, index) => {
|
width: 100%;
|
||||||
this.stdinbufferInt[startingIndex + index] = value
|
height: 180px;
|
||||||
})
|
|
||||||
|
|
||||||
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.message(`Exited with status: ${event.data.returnCode}`)
|
|
||||||
this.finishedCallback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WasmTerminal {
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.inputBuffer = new BufferQueue();
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
#info {
|
||||||
|
padding-top: 20px;
|
||||||
this.xterm.onData(this.handleTermData)
|
|
||||||
}
|
|
||||||
|
|
||||||
open(container) {
|
|
||||||
this.xterm.open(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTermData = (data) => {
|
|
||||||
const ord = data.charCodeAt(0);
|
|
||||||
data = data.replace(/\r(?!\n)/g, "\n") // Convert lone CRs to LF
|
|
||||||
|
|
||||||
// Handle pasted data
|
|
||||||
if (data.length > 1 && data.includes("\n")) {
|
|
||||||
let alreadyWrittenChars = 0;
|
|
||||||
// If line already had data on it, merge pasted data with it
|
|
||||||
if (this.input != '') {
|
|
||||||
this.inputBuffer.addData(this.input);
|
|
||||||
alreadyWrittenChars = this.input.length;
|
|
||||||
this.input = '';
|
|
||||||
}
|
}
|
||||||
this.inputBuffer.addData(data);
|
.button-container {
|
||||||
// If input is active, write the first line
|
display: flex;
|
||||||
if (this.activeInput) {
|
justify-content: end;
|
||||||
let line = this.inputBuffer.nextLine();
|
height: 50px;
|
||||||
this.writeLine(line.slice(alreadyWrittenChars));
|
align-items: center;
|
||||||
this.resolveInput(line);
|
gap: 10px;
|
||||||
this.activeInput = false;
|
|
||||||
}
|
}
|
||||||
// When input isn't active, add to line buffer
|
button {
|
||||||
} else if (!this.activeInput) {
|
padding: 6px 18px;
|
||||||
// Skip non-printable characters
|
|
||||||
if (!(ord === 0x1b || ord == 0x7f || ord < 32)) {
|
|
||||||
this.inputBuffer.addData(data);
|
|
||||||
}
|
}
|
||||||
// TODO: Handle ANSI escape sequences
|
</style>
|
||||||
} else if (ord === 0x1b) {
|
<script
|
||||||
// Handle special characters
|
src="https://unpkg.com/xterm@4.18.0/lib/xterm.js"
|
||||||
} else if (ord < 32 || ord === 0x7f) {
|
crossorigin
|
||||||
switch (data) {
|
integrity="sha384-yYdNmem1ioP5Onm7RpXutin5A8TimLheLNQ6tnMi01/ZpxXdAwIm2t4fJMx1Djs+"
|
||||||
case "\x0c": // CTRL+L
|
/>
|
||||||
this.clear();
|
<script type="module">
|
||||||
break;
|
class WorkerManager {
|
||||||
case "\n": // ENTER
|
constructor(
|
||||||
case "\x0a": // CTRL+J
|
workerURL,
|
||||||
case "\x0d": // CTRL+M
|
standardIO,
|
||||||
this.resolveInput(this.input + this.writeLine('\n'));
|
readyCallBack,
|
||||||
this.input = '';
|
finishedCallback,
|
||||||
this.activeInput = false;
|
) {
|
||||||
break;
|
this.workerURL = workerURL;
|
||||||
case "\x7F": // BACKSPACE
|
this.worker = null;
|
||||||
case "\x08": // CTRL+H
|
this.standardIO = standardIO;
|
||||||
this.handleCursorErase(true);
|
this.readyCallBack = readyCallBack;
|
||||||
break;
|
this.finishedCallback = finishedCallback;
|
||||||
case "\x04": // CTRL+D
|
|
||||||
// Send empty input
|
this.initialiseWorker();
|
||||||
if (this.input === '') {
|
}
|
||||||
this.resolveInput('')
|
|
||||||
this.activeInput = false;
|
async initialiseWorker() {
|
||||||
|
if (!this.worker) {
|
||||||
|
this.worker = new Worker(this.workerURL, {
|
||||||
|
type: "module",
|
||||||
|
});
|
||||||
|
this.worker.addEventListener(
|
||||||
|
"message",
|
||||||
|
this.handleMessageFromWorker,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(options) {
|
||||||
|
this.worker.postMessage({
|
||||||
|
type: "run",
|
||||||
|
args: options.args || [],
|
||||||
|
files: options.files || {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
if (this.worker) {
|
||||||
|
this.worker.terminate();
|
||||||
|
this.worker = null;
|
||||||
|
}
|
||||||
|
this.standardIO.message("Worker process terminated.");
|
||||||
|
this.initialiseWorker();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.message(
|
||||||
|
`Exited with status: ${event.data.returnCode}`,
|
||||||
|
);
|
||||||
|
this.finishedCallback();
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.handleCursorInsert(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeLine(line) {
|
class WasmTerminal {
|
||||||
this.xterm.write(line.slice(0, -1))
|
constructor() {
|
||||||
this.xterm.write('\r\n');
|
this.inputBuffer = new BufferQueue();
|
||||||
return line;
|
this.input = "";
|
||||||
}
|
this.resolveInput = null;
|
||||||
|
this.activeInput = false;
|
||||||
|
this.inputStartCursor = null;
|
||||||
|
|
||||||
handleCursorInsert(data) {
|
this.xterm = new Terminal({
|
||||||
this.input += data;
|
scrollback: 10000,
|
||||||
this.xterm.write(data)
|
fontSize: 14,
|
||||||
}
|
theme: { background: "#1a1c1f" },
|
||||||
|
cols: 100,
|
||||||
|
});
|
||||||
|
|
||||||
handleCursorErase() {
|
this.xterm.onKey((keyEvent) => {
|
||||||
// Don't delete past the start of input
|
// Fix for iOS Keyboard Jumping on space
|
||||||
if (this.xterm.buffer.active.cursorX <= this.inputStartCursor) {
|
if (keyEvent.key === " ") {
|
||||||
return
|
keyEvent.domEvent.preventDefault();
|
||||||
}
|
}
|
||||||
this.input = this.input.slice(0, -1)
|
});
|
||||||
this.xterm.write('\x1B[D')
|
|
||||||
this.xterm.write('\x1B[P')
|
|
||||||
}
|
|
||||||
|
|
||||||
prompt = async () => {
|
this.xterm.onData(this.handleTermData);
|
||||||
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)
|
open(container) {
|
||||||
// If line buffer has a line ready, send it immediately
|
this.xterm.open(container);
|
||||||
if (this.inputBuffer.hasLineReady()) {
|
}
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
resolve(this.writeLine(this.inputBuffer.nextLine()));
|
handleTermData = (data) => {
|
||||||
this.activeInput = false;
|
const ord = data.charCodeAt(0);
|
||||||
})
|
data = data.replace(/\r(?!\n)/g, "\n"); // Convert lone CRs to LF
|
||||||
// If line buffer has an incomplete line, use it for the active line
|
|
||||||
} else if (this.inputBuffer.lastLineIsIncomplete()) {
|
// Handle pasted data
|
||||||
// Hack to ensure cursor input start doesn't end up after user input
|
if (data.length > 1 && data.includes("\n")) {
|
||||||
setTimeout(() => {this.handleCursorInsert(this.inputBuffer.nextLine())}, 1);
|
let alreadyWrittenChars = 0;
|
||||||
}
|
// If line already had data on it, merge pasted data with it
|
||||||
return new Promise((resolve, reject) => {
|
if (this.input != "") {
|
||||||
this.resolveInput = (value) => {
|
this.inputBuffer.addData(this.input);
|
||||||
resolve(value)
|
alreadyWrittenChars = this.input.length;
|
||||||
|
this.input = "";
|
||||||
|
}
|
||||||
|
this.inputBuffer.addData(data);
|
||||||
|
// If input is active, write the first line
|
||||||
|
if (this.activeInput) {
|
||||||
|
let line = this.inputBuffer.nextLine();
|
||||||
|
this.writeLine(line.slice(alreadyWrittenChars));
|
||||||
|
this.resolveInput(line);
|
||||||
|
this.activeInput = false;
|
||||||
|
}
|
||||||
|
// When input isn't active, add to line buffer
|
||||||
|
} else if (!this.activeInput) {
|
||||||
|
// Skip non-printable characters
|
||||||
|
if (!(ord === 0x1b || ord == 0x7f || ord < 32)) {
|
||||||
|
this.inputBuffer.addData(data);
|
||||||
|
}
|
||||||
|
// TODO: Handle ANSI escape sequences
|
||||||
|
} else if (ord === 0x1b) {
|
||||||
|
// Handle special characters
|
||||||
|
} else if (ord < 32 || ord === 0x7f) {
|
||||||
|
switch (data) {
|
||||||
|
case "\x0c": // CTRL+L
|
||||||
|
this.clear();
|
||||||
|
break;
|
||||||
|
case "\n": // ENTER
|
||||||
|
case "\x0a": // CTRL+J
|
||||||
|
case "\x0d": // CTRL+M
|
||||||
|
this.resolveInput(
|
||||||
|
this.input + this.writeLine("\n"),
|
||||||
|
);
|
||||||
|
this.input = "";
|
||||||
|
this.activeInput = false;
|
||||||
|
break;
|
||||||
|
case "\x7F": // BACKSPACE
|
||||||
|
case "\x08": // CTRL+H
|
||||||
|
this.handleCursorErase(true);
|
||||||
|
break;
|
||||||
|
case "\x04": // CTRL+D
|
||||||
|
// Send empty input
|
||||||
|
if (this.input === "") {
|
||||||
|
this.resolveInput("");
|
||||||
|
this.activeInput = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.handleCursorInsert(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
writeLine(line) {
|
||||||
|
this.xterm.write(line.slice(0, -1));
|
||||||
|
this.xterm.write("\r\n");
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
// If line buffer has a line ready, send it immediately
|
||||||
|
if (this.inputBuffer.hasLineReady()) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
resolve(
|
||||||
|
this.writeLine(this.inputBuffer.nextLine()),
|
||||||
|
);
|
||||||
|
this.activeInput = false;
|
||||||
|
});
|
||||||
|
// If line buffer has an incomplete line, use it for the active line
|
||||||
|
} else if (this.inputBuffer.lastLineIsIncomplete()) {
|
||||||
|
// Hack to ensure cursor input start doesn't end up after user input
|
||||||
|
setTimeout(() => {
|
||||||
|
this.handleCursorInsert(
|
||||||
|
this.inputBuffer.nextLine(),
|
||||||
|
);
|
||||||
|
}, 1);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.resolveInput = (value) => {
|
||||||
|
resolve(value);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.xterm.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
print(charCode) {
|
||||||
|
let array = [charCode];
|
||||||
|
if (charCode == 10) {
|
||||||
|
array = [13, 10]; // Replace \n with \r\n
|
||||||
|
}
|
||||||
|
this.xterm.write(new Uint8Array(array));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
clear() {
|
class BufferQueue {
|
||||||
this.xterm.clear();
|
constructor(xterm) {
|
||||||
}
|
this.buffer = [];
|
||||||
|
}
|
||||||
|
|
||||||
print(charCode) {
|
isEmpty() {
|
||||||
let array = [charCode];
|
return this.buffer.length == 0;
|
||||||
if (charCode == 10) {
|
}
|
||||||
array = [13, 10]; // Replace \n with \r\n
|
|
||||||
}
|
|
||||||
this.xterm.write(new Uint8Array(array));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BufferQueue {
|
lastLineIsIncomplete() {
|
||||||
constructor(xterm) {
|
return (
|
||||||
this.buffer = []
|
!this.isEmpty() &&
|
||||||
}
|
!this.buffer[this.buffer.length - 1].endsWith("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
isEmpty() {
|
hasLineReady() {
|
||||||
return this.buffer.length == 0
|
return !this.isEmpty() && this.buffer[0].endsWith("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
lastLineIsIncomplete() {
|
addData(data) {
|
||||||
return !this.isEmpty() && !this.buffer[this.buffer.length-1].endsWith("\n")
|
let lines = data.match(/.*(\n|$)/g);
|
||||||
}
|
if (this.lastLineIsIncomplete()) {
|
||||||
|
this.buffer[this.buffer.length - 1] += lines.shift();
|
||||||
|
}
|
||||||
|
for (let line of lines) {
|
||||||
|
this.buffer.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hasLineReady() {
|
nextLine() {
|
||||||
return !this.isEmpty() && this.buffer[0].endsWith("\n")
|
return this.buffer.shift();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
addData(data) {
|
const runButton = document.getElementById("run");
|
||||||
let lines = data.match(/.*(\n|$)/g)
|
const replButton = document.getElementById("repl");
|
||||||
if (this.lastLineIsIncomplete()) {
|
const stopButton = document.getElementById("stop");
|
||||||
this.buffer[this.buffer.length-1] += lines.shift()
|
const clearButton = document.getElementById("clear");
|
||||||
}
|
|
||||||
for (let line of lines) {
|
|
||||||
this.buffer.push(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextLine() {
|
const codeBox = document.getElementById("codebox");
|
||||||
return this.buffer.shift()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runButton = document.getElementById('run')
|
window.onload = () => {
|
||||||
const replButton = document.getElementById('repl')
|
const terminal = new WasmTerminal();
|
||||||
const stopButton = document.getElementById('stop')
|
terminal.open(document.getElementById("terminal"));
|
||||||
const clearButton = document.getElementById('clear')
|
|
||||||
|
|
||||||
const codeBox = document.getElementById('codebox')
|
const stdio = {
|
||||||
|
stdout: (charCode) => {
|
||||||
|
terminal.print(charCode);
|
||||||
|
},
|
||||||
|
stderr: (charCode) => {
|
||||||
|
terminal.print(charCode);
|
||||||
|
},
|
||||||
|
stdin: async () => {
|
||||||
|
return await terminal.prompt();
|
||||||
|
},
|
||||||
|
message: (text) => {
|
||||||
|
terminal.writeLine(`\r\n${text}\r\n`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
window.onload = () => {
|
const programRunning = (isRunning) => {
|
||||||
const terminal = new WasmTerminal()
|
if (isRunning) {
|
||||||
terminal.open(document.getElementById('terminal'))
|
replButton.setAttribute("disabled", true);
|
||||||
|
runButton.setAttribute("disabled", true);
|
||||||
|
stopButton.removeAttribute("disabled");
|
||||||
|
} else {
|
||||||
|
replButton.removeAttribute("disabled");
|
||||||
|
runButton.removeAttribute("disabled");
|
||||||
|
stopButton.setAttribute("disabled", true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const stdio = {
|
runButton.addEventListener("click", (e) => {
|
||||||
stdout: (charCode) => { terminal.print(charCode) },
|
terminal.clear();
|
||||||
stderr: (charCode) => { terminal.print(charCode) },
|
programRunning(true);
|
||||||
stdin: async () => {
|
const code = codeBox.value;
|
||||||
return await terminal.prompt()
|
pythonWorkerManager.run({
|
||||||
},
|
args: ["main.py"],
|
||||||
message: (text) => { terminal.writeLine(`\r\n${text}\r\n`) },
|
files: { "main.py": code },
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const programRunning = (isRunning) => {
|
replButton.addEventListener("click", (e) => {
|
||||||
if (isRunning) {
|
terminal.clear();
|
||||||
replButton.setAttribute('disabled', true)
|
programRunning(true);
|
||||||
runButton.setAttribute('disabled', true)
|
// Need to use "-i -" to force interactive mode.
|
||||||
stopButton.removeAttribute('disabled')
|
// Looks like isatty always returns false in emscripten
|
||||||
} else {
|
pythonWorkerManager.run({ args: ["-i", "-"], files: {} });
|
||||||
replButton.removeAttribute('disabled')
|
});
|
||||||
runButton.removeAttribute('disabled')
|
|
||||||
stopButton.setAttribute('disabled', true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
runButton.addEventListener('click', (e) => {
|
stopButton.addEventListener("click", (e) => {
|
||||||
terminal.clear()
|
programRunning(false);
|
||||||
programRunning(true)
|
pythonWorkerManager.reset();
|
||||||
const code = codeBox.value
|
});
|
||||||
pythonWorkerManager.run({args: ['main.py'], files: {'main.py': code}})
|
|
||||||
})
|
|
||||||
|
|
||||||
replButton.addEventListener('click', (e) => {
|
clearButton.addEventListener("click", (e) => {
|
||||||
terminal.clear()
|
terminal.clear();
|
||||||
programRunning(true)
|
});
|
||||||
// Need to use "-i -" to force interactive mode.
|
|
||||||
// Looks like isatty always returns false in emscripten
|
|
||||||
pythonWorkerManager.run({args: ['-i', '-'], files: {}})
|
|
||||||
})
|
|
||||||
|
|
||||||
stopButton.addEventListener('click', (e) => {
|
const readyCallback = () => {
|
||||||
programRunning(false)
|
replButton.removeAttribute("disabled");
|
||||||
pythonWorkerManager.reset()
|
runButton.removeAttribute("disabled");
|
||||||
})
|
clearButton.removeAttribute("disabled");
|
||||||
|
};
|
||||||
|
|
||||||
clearButton.addEventListener('click', (e) => {
|
const finishedCallback = () => {
|
||||||
terminal.clear()
|
programRunning(false);
|
||||||
})
|
};
|
||||||
|
|
||||||
const readyCallback = () => {
|
const pythonWorkerManager = new WorkerManager(
|
||||||
replButton.removeAttribute('disabled')
|
"./python.worker.mjs",
|
||||||
runButton.removeAttribute('disabled')
|
stdio,
|
||||||
clearButton.removeAttribute('disabled')
|
readyCallback,
|
||||||
}
|
finishedCallback,
|
||||||
|
);
|
||||||
const finishedCallback = () => {
|
};
|
||||||
programRunning(false)
|
</script>
|
||||||
}
|
</head>
|
||||||
|
<body>
|
||||||
const pythonWorkerManager = new WorkerManager('./python.worker.mjs', stdio, readyCallback, finishedCallback)
|
<h1>Simple REPL for Python WASM</h1>
|
||||||
}
|
<textarea id="codebox" cols="108" rows="16">
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Simple REPL for Python WASM</h1>
|
|
||||||
<textarea id="codebox" cols="108" rows="16">
|
|
||||||
print('Welcome to WASM!')
|
print('Welcome to WASM!')
|
||||||
</textarea>
|
</textarea
|
||||||
<div class="button-container">
|
>
|
||||||
<button id="run" disabled>Run</button>
|
<div class="button-container">
|
||||||
<button id="repl" disabled>Start REPL</button>
|
<button id="run" disabled>Run</button>
|
||||||
<button id="stop" disabled>Stop</button>
|
<button id="repl" disabled>Start REPL</button>
|
||||||
<button id="clear" disabled>Clear</button>
|
<button id="stop" disabled>Stop</button>
|
||||||
</div>
|
<button id="clear" disabled>Clear</button>
|
||||||
<div id="terminal"></div>
|
</div>
|
||||||
<div id="info">
|
<div id="terminal"></div>
|
||||||
The simple REPL provides a limited Python experience in the browser.
|
<div id="info">
|
||||||
<a href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md">
|
The simple REPL provides a limited Python experience in the browser.
|
||||||
Tools/wasm/README.md</a> contains a list of known limitations and
|
<a
|
||||||
issues. Networking, subprocesses, and threading are not available.
|
href="https://github.com/python/cpython/blob/main/Tools/wasm/README.md"
|
||||||
</div>
|
>
|
||||||
</body>
|
Tools/wasm/README.md
|
||||||
|
</a>
|
||||||
|
contains a list of known limitations and issues. Networking,
|
||||||
|
subprocesses, and threading are not available.
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,104 +1,109 @@
|
||||||
import createEmscriptenModule from "./python.mjs";
|
import createEmscriptenModule from "./python.mjs";
|
||||||
|
|
||||||
class StdinBuffer {
|
class StdinBuffer {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sab = new SharedArrayBuffer(128 * Int32Array.BYTES_PER_ELEMENT)
|
this.sab = new SharedArrayBuffer(128 * Int32Array.BYTES_PER_ELEMENT);
|
||||||
this.buffer = new Int32Array(this.sab)
|
this.buffer = new Int32Array(this.sab);
|
||||||
this.readIndex = 1;
|
this.readIndex = 1;
|
||||||
this.numberOfCharacters = 0;
|
this.numberOfCharacters = 0;
|
||||||
this.sentNull = true
|
this.sentNull = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt() {
|
prompt() {
|
||||||
this.readIndex = 1
|
this.readIndex = 1;
|
||||||
Atomics.store(this.buffer, 0, -1)
|
Atomics.store(this.buffer, 0, -1);
|
||||||
postMessage({
|
postMessage({
|
||||||
type: 'stdin',
|
type: "stdin",
|
||||||
buffer: this.sab
|
buffer: this.sab,
|
||||||
})
|
});
|
||||||
Atomics.wait(this.buffer, 0, -1)
|
Atomics.wait(this.buffer, 0, -1);
|
||||||
this.numberOfCharacters = this.buffer[0]
|
this.numberOfCharacters = this.buffer[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
stdin = () => {
|
stdin = () => {
|
||||||
while (this.numberOfCharacters + 1 === this.readIndex) {
|
while (this.numberOfCharacters + 1 === this.readIndex) {
|
||||||
if (!this.sentNull) {
|
if (!this.sentNull) {
|
||||||
// Must return null once to indicate we're done for now.
|
// Must return null once to indicate we're done for now.
|
||||||
this.sentNull = true
|
this.sentNull = true;
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
this.sentNull = false
|
this.sentNull = false;
|
||||||
// Prompt will reset this.readIndex to 1
|
// Prompt will reset this.readIndex to 1
|
||||||
this.prompt()
|
this.prompt();
|
||||||
}
|
|
||||||
const char = this.buffer[this.readIndex]
|
|
||||||
this.readIndex += 1
|
|
||||||
return char
|
|
||||||
}
|
}
|
||||||
|
const char = this.buffer[this.readIndex];
|
||||||
|
this.readIndex += 1;
|
||||||
|
return char;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const stdout = (charCode) => {
|
const stdout = (charCode) => {
|
||||||
if (charCode) {
|
if (charCode) {
|
||||||
postMessage({
|
postMessage({
|
||||||
type: 'stdout',
|
type: "stdout",
|
||||||
stdout: charCode,
|
stdout: charCode,
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(typeof charCode, charCode)
|
console.log(typeof charCode, charCode);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const stderr = (charCode) => {
|
const stderr = (charCode) => {
|
||||||
if (charCode) {
|
if (charCode) {
|
||||||
postMessage({
|
postMessage({
|
||||||
type: 'stderr',
|
type: "stderr",
|
||||||
stderr: charCode,
|
stderr: charCode,
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(typeof charCode, charCode)
|
console.log(typeof charCode, charCode);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const stdinBuffer = new StdinBuffer()
|
const stdinBuffer = new StdinBuffer();
|
||||||
|
|
||||||
const emscriptenSettings = {
|
const emscriptenSettings = {
|
||||||
noInitialRun: true,
|
noInitialRun: true,
|
||||||
stdin: stdinBuffer.stdin,
|
stdin: stdinBuffer.stdin,
|
||||||
stdout: stdout,
|
stdout: stdout,
|
||||||
stderr: stderr,
|
stderr: stderr,
|
||||||
onRuntimeInitialized: () => {
|
onRuntimeInitialized: () => {
|
||||||
postMessage({type: 'ready', stdinBuffer: stdinBuffer.sab})
|
postMessage({ type: "ready", stdinBuffer: stdinBuffer.sab });
|
||||||
},
|
},
|
||||||
async preRun(Module) {
|
async preRun(Module) {
|
||||||
const versionHex = Module.HEAPU32[Module._Py_Version/4].toString(16);
|
const versionHex = Module.HEAPU32[Module._Py_Version / 4].toString(16);
|
||||||
const versionTuple = versionHex.padStart(8, "0").match(/.{1,2}/g).map((x) => parseInt(x, 16));
|
const versionTuple = versionHex
|
||||||
const [major, minor, ..._] = versionTuple;
|
.padStart(8, "0")
|
||||||
// Prevent complaints about not finding exec-prefix by making a lib-dynload directory
|
.match(/.{1,2}/g)
|
||||||
Module.FS.mkdirTree(`/lib/python${major}.${minor}/lib-dynload/`);
|
.map((x) => parseInt(x, 16));
|
||||||
Module.addRunDependency("install-stdlib");
|
const [major, minor, ..._] = versionTuple;
|
||||||
const resp = await fetch(`python${major}.${minor}.zip`);
|
// Prevent complaints about not finding exec-prefix by making a lib-dynload directory
|
||||||
const stdlibBuffer = await resp.arrayBuffer();
|
Module.FS.mkdirTree(`/lib/python${major}.${minor}/lib-dynload/`);
|
||||||
Module.FS.writeFile(`/lib/python${major}${minor}.zip`, new Uint8Array(stdlibBuffer), { canOwn: true });
|
Module.addRunDependency("install-stdlib");
|
||||||
Module.removeRunDependency("install-stdlib");
|
const resp = await fetch(`python${major}.${minor}.zip`);
|
||||||
}
|
const stdlibBuffer = await resp.arrayBuffer();
|
||||||
}
|
Module.FS.writeFile(
|
||||||
|
`/lib/python${major}${minor}.zip`,
|
||||||
|
new Uint8Array(stdlibBuffer),
|
||||||
|
{ canOwn: true },
|
||||||
|
);
|
||||||
|
Module.removeRunDependency("install-stdlib");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const modulePromise = createEmscriptenModule(emscriptenSettings);
|
const modulePromise = createEmscriptenModule(emscriptenSettings);
|
||||||
|
|
||||||
|
|
||||||
onmessage = async (event) => {
|
onmessage = async (event) => {
|
||||||
if (event.data.type === 'run') {
|
if (event.data.type === "run") {
|
||||||
const Module = await modulePromise;
|
const Module = await modulePromise;
|
||||||
if (event.data.files) {
|
if (event.data.files) {
|
||||||
for (const [filename, contents] of Object.entries(event.data.files)) {
|
for (const [filename, contents] of Object.entries(event.data.files)) {
|
||||||
Module.FS.writeFile(filename, contents)
|
Module.FS.writeFile(filename, contents);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
const ret = Module.callMain(event.data.args);
|
|
||||||
postMessage({
|
|
||||||
type: 'finished',
|
|
||||||
returnCode: ret
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
const ret = Module.callMain(event.data.args);
|
||||||
|
postMessage({
|
||||||
|
type: "finished",
|
||||||
|
returnCode: ret,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue