Skip to content

Commit

Permalink
gh-84461: Improve WebAssembly in-browser demo (#91879)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
treyhunner and tiran authored Jul 1, 2022
1 parent 5f2c91a commit a8e333d
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 25 deletions.
7 changes: 7 additions & 0 deletions Tools/wasm/.editorconfig
Original file line number Diff line number Diff line change
@@ -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
109 changes: 90 additions & 19 deletions Tools/wasm/python.html
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
class WasmTerminal {

constructor() {
this.inputBuffer = new BufferQueue();
this.input = ''
this.resolveInput = null
this.activeInput = false
Expand All @@ -123,28 +124,47 @@
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
Expand All @@ -157,6 +177,12 @@
}
}

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)
Expand All @@ -176,9 +202,19 @@
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)
}
})
Expand All @@ -188,9 +224,44 @@
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()
}
}

Expand All @@ -202,8 +273,8 @@
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()
}
Expand Down
8 changes: 2 additions & 6 deletions Tools/wasm/python.worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -54,7 +50,7 @@ const stderr = (charCode) => {
if (charCode) {
postMessage({
type: 'stderr',
stderr: String.fromCharCode(charCode),
stderr: charCode,
})
} else {
console.log(typeof charCode, charCode)
Expand Down

0 comments on commit a8e333d

Please sign in to comment.