mirror of
https://github.com/python/cpython.git
synced 2025-09-18 14:40:43 +00:00
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>
This commit is contained in:
parent
5f2c91a343
commit
a8e333d79a
3 changed files with 99 additions and 25 deletions
7
Tools/wasm/.editorconfig
Normal file
7
Tools/wasm/.editorconfig
Normal file
|
@ -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
|
|
@ -100,6 +100,7 @@ class WorkerManager {
|
||||||
class WasmTerminal {
|
class WasmTerminal {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.inputBuffer = new BufferQueue();
|
||||||
this.input = ''
|
this.input = ''
|
||||||
this.resolveInput = null
|
this.resolveInput = null
|
||||||
this.activeInput = false
|
this.activeInput = false
|
||||||
|
@ -123,28 +124,47 @@ class WasmTerminal {
|
||||||
this.xterm.open(container);
|
this.xterm.open(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReadComplete(lastChar) {
|
|
||||||
this.resolveInput(this.input + lastChar)
|
|
||||||
this.activeInput = false
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTermData = (data) => {
|
handleTermData = (data) => {
|
||||||
if (!this.activeInput) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const ord = data.charCodeAt(0);
|
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
|
// TODO: Handle ANSI escape sequences
|
||||||
if (ord === 0x1b) {
|
} else if (ord === 0x1b) {
|
||||||
// Handle special characters
|
// Handle special characters
|
||||||
} else if (ord < 32 || ord === 0x7f) {
|
} else if (ord < 32 || ord === 0x7f) {
|
||||||
switch (data) {
|
switch (data) {
|
||||||
case "\r": // ENTER
|
case "\x0c": // CTRL+L
|
||||||
|
this.clear();
|
||||||
|
break;
|
||||||
|
case "\n": // ENTER
|
||||||
case "\x0a": // CTRL+J
|
case "\x0a": // CTRL+J
|
||||||
case "\x0d": // CTRL+M
|
case "\x0d": // CTRL+M
|
||||||
this.xterm.write('\r\n');
|
this.resolveInput(this.input + this.writeLine('\n'));
|
||||||
this.handleReadComplete('\n');
|
this.input = '';
|
||||||
|
this.activeInput = false;
|
||||||
break;
|
break;
|
||||||
case "\x7F": // BACKSPACE
|
case "\x7F": // BACKSPACE
|
||||||
case "\x08": // CTRL+H
|
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) {
|
handleCursorInsert(data) {
|
||||||
this.input += data;
|
this.input += data;
|
||||||
this.xterm.write(data)
|
this.xterm.write(data)
|
||||||
|
@ -176,9 +202,19 @@ class WasmTerminal {
|
||||||
this.activeInput = true
|
this.activeInput = true
|
||||||
// Hack to allow stdout/stderr to finish before we figure out where input starts
|
// Hack to allow stdout/stderr to finish before we figure out where input starts
|
||||||
setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1)
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.resolveInput = (value) => {
|
this.resolveInput = (value) => {
|
||||||
this.input = ''
|
|
||||||
resolve(value)
|
resolve(value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -188,9 +224,44 @@ class WasmTerminal {
|
||||||
this.xterm.clear();
|
this.xterm.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
print(message) {
|
print(charCode) {
|
||||||
const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n");
|
let array = [charCode];
|
||||||
this.xterm.write(normInput);
|
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'))
|
terminal.open(document.getElementById('terminal'))
|
||||||
|
|
||||||
const stdio = {
|
const stdio = {
|
||||||
stdout: (s) => { terminal.print(s) },
|
stdout: (charCode) => { terminal.print(charCode) },
|
||||||
stderr: (s) => { terminal.print(s) },
|
stderr: (charCode) => { terminal.print(charCode) },
|
||||||
stdin: async () => {
|
stdin: async () => {
|
||||||
return await terminal.prompt()
|
return await terminal.prompt()
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,15 +35,11 @@ class StdinBuffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stdoutBufSize = 128;
|
|
||||||
const stdoutBuf = new Int32Array()
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
const stdout = (charCode) => {
|
const stdout = (charCode) => {
|
||||||
if (charCode) {
|
if (charCode) {
|
||||||
postMessage({
|
postMessage({
|
||||||
type: 'stdout',
|
type: 'stdout',
|
||||||
stdout: String.fromCharCode(charCode),
|
stdout: charCode,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log(typeof charCode, charCode)
|
console.log(typeof charCode, charCode)
|
||||||
|
@ -54,7 +50,7 @@ const stderr = (charCode) => {
|
||||||
if (charCode) {
|
if (charCode) {
|
||||||
postMessage({
|
postMessage({
|
||||||
type: 'stderr',
|
type: 'stderr',
|
||||||
stderr: String.fromCharCode(charCode),
|
stderr: charCode,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log(typeof charCode, charCode)
|
console.log(typeof charCode, charCode)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue