summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTrey Hunner <trey@treyhunner.com>2022-07-01 09:52:58 (GMT)
committerGitHub <noreply@github.com>2022-07-01 09:52:58 (GMT)
commita8e333d79aa639417e496181bcbad2cb801a7a56 (patch)
treed136e1ebf4dc07e41c52147b6cb1fe9a7bfd4a47
parent5f2c91a343fb20f8c2fc78cbb3e68234bcba40a8 (diff)
downloadcpython-a8e333d79aa639417e496181bcbad2cb801a7a56.zip
cpython-a8e333d79aa639417e496181bcbad2cb801a7a56.tar.gz
cpython-a8e333d79aa639417e496181bcbad2cb801a7a56.tar.bz2
gh-84461: Improve WebAssembly in-browser demo (#91879)
* Buffer standard input line-by-line * Add non-root .editorconfig for JS & HTML indent * Add support for clearing REPL with CTRL+L * Support unicode in stdout and stderr * Remove \r\n normalization * Note that local .editorconfig file extends root * Only normalize lone \r characters (convert to \n) * Skip non-printable characters in buffered input * Fix Safari bug (regex lookbehind not supported) Co-authored-by: Christian Heimes <christian@python.org>
-rw-r--r--Tools/wasm/.editorconfig7
-rw-r--r--Tools/wasm/python.html109
-rw-r--r--Tools/wasm/python.worker.js8
3 files changed, 99 insertions, 25 deletions
diff --git a/Tools/wasm/.editorconfig b/Tools/wasm/.editorconfig
new file mode 100644
index 0000000..da1aa6a
--- /dev/null
+++ b/Tools/wasm/.editorconfig
@@ -0,0 +1,7 @@
+root = false # This extends the root .editorconfig
+
+[*.{html,js}]
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = space
+indent_size = 4
diff --git a/Tools/wasm/python.html b/Tools/wasm/python.html
index c8d1748..41cf5fc 100644
--- a/Tools/wasm/python.html
+++ b/Tools/wasm/python.html
@@ -100,6 +100,7 @@ class WorkerManager {
class WasmTerminal {
constructor() {
+ this.inputBuffer = new BufferQueue();
this.input = ''
this.resolveInput = null
this.activeInput = false
@@ -123,28 +124,47 @@ class WasmTerminal {
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;
+ 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);
+ // 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
- if (ord === 0x1b) {
+ } else if (ord === 0x1b) {
// Handle special characters
} else if (ord < 32 || ord === 0x7f) {
switch (data) {
- case "\r": // ENTER
+ case "\x0c": // CTRL+L
+ this.clear();
+ break;
+ case "\n": // ENTER
case "\x0a": // CTRL+J
case "\x0d": // CTRL+M
- this.xterm.write('\r\n');
- this.handleReadComplete('\n');
+ this.resolveInput(this.input + this.writeLine('\n'));
+ this.input = '';
+ this.activeInput = false;
break;
case "\x7F": // BACKSPACE
case "\x08": // CTRL+H
@@ -157,6 +177,12 @@ class WasmTerminal {
}
}
+ 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)
@@ -176,9 +202,19 @@ class WasmTerminal {
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) => {
- this.input = ''
resolve(value)
}
})
@@ -188,9 +224,44 @@ class WasmTerminal {
this.xterm.clear();
}
- print(message) {
- const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n");
- this.xterm.write(normInput);
+ print(charCode) {
+ let array = [charCode];
+ if (charCode == 10) {
+ array = [13, 10]; // Replace \n with \r\n
+ }
+ this.xterm.write(new Uint8Array(array));
+ }
+}
+
+class BufferQueue {
+ constructor(xterm) {
+ this.buffer = []
+ }
+
+ isEmpty() {
+ return this.buffer.length == 0
+ }
+
+ lastLineIsIncomplete() {
+ return !this.isEmpty() && !this.buffer[this.buffer.length-1].endsWith("\n")
+ }
+
+ hasLineReady() {
+ return !this.isEmpty() && this.buffer[0].endsWith("\n")
+ }
+
+ addData(data) {
+ 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)
+ }
+ }
+
+ nextLine() {
+ return this.buffer.shift()
}
}
@@ -202,8 +273,8 @@ window.onload = () => {
terminal.open(document.getElementById('terminal'))
const stdio = {
- stdout: (s) => { terminal.print(s) },
- stderr: (s) => { terminal.print(s) },
+ stdout: (charCode) => { terminal.print(charCode) },
+ stderr: (charCode) => { terminal.print(charCode) },
stdin: async () => {
return await terminal.prompt()
}
diff --git a/Tools/wasm/python.worker.js b/Tools/wasm/python.worker.js
index c3a8bdf..1b79460 100644
--- a/Tools/wasm/python.worker.js
+++ b/Tools/wasm/python.worker.js
@@ -35,15 +35,11 @@ class StdinBuffer {
}
}
-const stdoutBufSize = 128;
-const stdoutBuf = new Int32Array()
-let index = 0;
-
const stdout = (charCode) => {
if (charCode) {
postMessage({
type: 'stdout',
- stdout: String.fromCharCode(charCode),
+ stdout: charCode,
})
} else {
console.log(typeof charCode, charCode)
@@ -54,7 +50,7 @@ const stderr = (charCode) => {
if (charCode) {
postMessage({
type: 'stderr',
- stderr: String.fromCharCode(charCode),
+ stderr: charCode,
})
} else {
console.log(typeof charCode, charCode)