Skip to content

Commit aab9ccc

Browse files
committed
WASM REPL: basic navigation in REPL (arrow keys, home/end, etc)
1 parent 394815e commit aab9ccc

File tree

1 file changed

+149
-9
lines changed

1 file changed

+149
-9
lines changed

Tools/wasm/emscripten/web_example/python.html

Lines changed: 149 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,7 @@
132132

133133
class WasmTerminal {
134134
constructor() {
135-
this.inputBuffer = new BufferQueue();
136-
this.input = "";
137-
this.resolveInput = null;
138-
this.activeInput = false;
139-
this.inputStartCursor = null;
135+
this.reset()
140136

141137
this.xterm = new Terminal({
142138
scrollback: 10000,
@@ -155,6 +151,19 @@
155151
this.xterm.onData(this.handleTermData);
156152
}
157153

154+
reset(){
155+
this.inputBuffer = new BufferQueue();
156+
this.input = "";
157+
this.resolveInput = null;
158+
this.activeInput = false;
159+
this.inputStartCursor = null;
160+
161+
this.cursorPosition = 0;
162+
this.history = [];
163+
this.historyIndex = -1;
164+
this.beforeHistoryNav = "";
165+
}
166+
158167
open(container) {
159168
this.xterm.open(container);
160169
}
@@ -186,9 +195,34 @@
186195
if (!(ord === 0x1b || ord == 0x7f || ord < 32)) {
187196
this.inputBuffer.addData(data);
188197
}
189-
// TODO: Handle ANSI escape sequences
198+
// TODO: Handle more escape sequences?
190199
} else if (ord === 0x1b) {
191200
// Handle special characters
201+
switch(data.slice(1)){
202+
case '[A': // up
203+
this.historyBack();
204+
break;
205+
case '[B': // down
206+
this.historyForward();
207+
break;
208+
case '[C': // right
209+
this.cursorRight();
210+
break;
211+
case '[D': // left
212+
this.cursorLeft();
213+
break;
214+
case '[H': // home key
215+
this.cursorHome();
216+
break;
217+
case '[F': // end key
218+
this.cursorEnd();
219+
break;
220+
case '[3~': // delete key
221+
this.deleteAtCursor();
222+
break;
223+
default:
224+
break;
225+
}
192226
} else if (ord < 32 || ord === 0x7f) {
193227
switch (data) {
194228
case "\x0c": // CTRL+L
@@ -226,8 +260,14 @@
226260
}
227261

228262
handleCursorInsert(data) {
229-
this.input += data;
263+
const trailing = this.input.slice(this.cursorPosition);
264+
this.input = this.input.slice(0, this.cursorPosition) + data + trailing;
265+
this.cursorPosition += data.length;
230266
this.xterm.write(data);
267+
if (trailing.length !== 0){
268+
this.xterm.write(trailing);
269+
this.xterm.write('\x1b[' + trailing.length + 'D');
270+
}
231271
}
232272

233273
handleCursorErase() {
@@ -238,9 +278,102 @@
238278
) {
239279
return;
240280
}
241-
this.input = this.input.slice(0, -1);
281+
const trailing = this.input.slice(this.cursorPosition);
282+
this.input = this.input.slice(0, this.cursorPosition - 1) + trailing;
283+
this.cursorPosition -= 1;
242284
this.xterm.write("\x1B[D");
243-
this.xterm.write("\x1B[P");
285+
this.xterm.write("\x1B[K");
286+
if (trailing){
287+
this.xterm.write(trailing);
288+
this.xterm.write('\x1b[' + trailing.length + 'D');
289+
}
290+
}
291+
292+
deleteAtCursor(){
293+
if (this.cursorPosition < this.input.length){
294+
const trailing = this.input.slice(this.cursorPosition + 1);
295+
this.input = this.input.slice(0, this.cursorPosition) + trailing;
296+
this.xterm.write("\x1B[K");
297+
if (trailing){
298+
this.xterm.write(trailing);
299+
this.xterm.write('\x1b[' + trailing.length + 'D');
300+
}
301+
}
302+
}
303+
304+
historyBack(){
305+
if (this.history.length === 0){
306+
return;
307+
}else if (this.historyIndex === -1){
308+
// we're not currently navigating the history; store
309+
// the current command and then look at the end of our
310+
// history buffer
311+
this.beforeHistoryNav = this.input;
312+
this.historyIndex = this.history.length - 1;
313+
}else if (this.historyIndex > 0){
314+
this.historyIndex -= 1;
315+
}
316+
this.input = this.history[this.historyIndex];
317+
// jump back to the start of the line
318+
if (this.cursorPosition > 0){
319+
this.xterm.write('\x1b[' + (this.cursorPosition) + 'D');
320+
}
321+
// clear the line
322+
this.xterm.write('\x1b[K')
323+
this.xterm.write(this.input);
324+
this.cursorPosition = this.input.length;
325+
}
326+
327+
historyForward(){
328+
if (this.history.length === 0 || this.historyIndex === -1){
329+
// we're not currently navigating the history; NOP.
330+
return;
331+
}else if (this.historyIndex < this.history.length - 1){
332+
this.historyIndex += 1;
333+
this.input = this.history[this.historyIndex];
334+
}else if (this.historyIndex == this.history.length - 1){
335+
// we're coming back from the last history value; reset
336+
// the input to whatever it was when we started going
337+
// through the history
338+
this.input = this.beforeHistoryNav;
339+
this.historyIndex = -1;
340+
}
341+
// jump back to the start of the line
342+
if (this.cursorPosition > 0){
343+
this.xterm.write('\x1b[' + (this.cursorPosition) + 'D');
344+
}
345+
// clear the line
346+
this.xterm.write('\x1b[K')
347+
this.xterm.write(this.input);
348+
this.cursorPosition = this.input.length;
349+
}
350+
351+
cursorRight(){
352+
if (this.cursorPosition < this.input.length){
353+
this.cursorPosition += 1;
354+
this.xterm.write('\x1b[C');
355+
}
356+
}
357+
358+
cursorLeft(){
359+
if (this.cursorPosition > 0){
360+
this.cursorPosition -= 1;
361+
this.xterm.write('\x1b[D');
362+
}
363+
}
364+
365+
cursorHome() {
366+
if (this.cursorPosition > 0){
367+
this.xterm.write('\x1b[' + this.cursorPosition + 'D');
368+
this.cursorPosition = 0;
369+
}
370+
}
371+
372+
cursorEnd() {
373+
if (this.cursorPosition < this.input.length){
374+
this.xterm.write('\x1b[' + (this.input.length - this.cursorPosition) + 'C');
375+
this.cursorPosition = this.input.length;
376+
}
244377
}
245378

246379
prompt = async () => {
@@ -269,6 +402,11 @@
269402
}
270403
return new Promise((resolve, reject) => {
271404
this.resolveInput = (value) => {
405+
if (value !== ""){
406+
this.history.push(value.slice(0, -1));
407+
this.historyIndex = -1;
408+
this.cursorPosition = 0;
409+
}
272410
resolve(value);
273411
};
274412
});
@@ -362,6 +500,7 @@
362500

363501
runButton.addEventListener("click", (e) => {
364502
terminal.clear();
503+
terminal.reset(); // reset the history
365504
programRunning(true);
366505
const code = codeBox.value;
367506
pythonWorkerManager.run({
@@ -372,6 +511,7 @@
372511

373512
replButton.addEventListener("click", (e) => {
374513
terminal.clear();
514+
terminal.reset(); // reset the history
375515
programRunning(true);
376516
// Need to use "-i -" to force interactive mode.
377517
// Looks like isatty always returns false in emscripten

0 commit comments

Comments
 (0)