|
132 | 132 |
|
133 | 133 | class WasmTerminal { |
134 | 134 | constructor() { |
135 | | - this.inputBuffer = new BufferQueue(); |
136 | | - this.input = ""; |
137 | | - this.resolveInput = null; |
138 | | - this.activeInput = false; |
139 | | - this.inputStartCursor = null; |
| 135 | + this.reset() |
140 | 136 |
|
141 | 137 | this.xterm = new Terminal({ |
142 | 138 | scrollback: 10000, |
|
155 | 151 | this.xterm.onData(this.handleTermData); |
156 | 152 | } |
157 | 153 |
|
| 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 | + |
158 | 167 | open(container) { |
159 | 168 | this.xterm.open(container); |
160 | 169 | } |
|
186 | 195 | if (!(ord === 0x1b || ord == 0x7f || ord < 32)) { |
187 | 196 | this.inputBuffer.addData(data); |
188 | 197 | } |
189 | | - // TODO: Handle ANSI escape sequences |
| 198 | + // TODO: Handle more escape sequences? |
190 | 199 | } else if (ord === 0x1b) { |
191 | 200 | // 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 | + } |
192 | 226 | } else if (ord < 32 || ord === 0x7f) { |
193 | 227 | switch (data) { |
194 | 228 | case "\x0c": // CTRL+L |
|
226 | 260 | } |
227 | 261 |
|
228 | 262 | 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; |
230 | 266 | this.xterm.write(data); |
| 267 | + if (trailing.length !== 0){ |
| 268 | + this.xterm.write(trailing); |
| 269 | + this.xterm.write('\x1b[' + trailing.length + 'D'); |
| 270 | + } |
231 | 271 | } |
232 | 272 |
|
233 | 273 | handleCursorErase() { |
|
238 | 278 | ) { |
239 | 279 | return; |
240 | 280 | } |
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; |
242 | 284 | 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 | + } |
244 | 377 | } |
245 | 378 |
|
246 | 379 | prompt = async () => { |
|
269 | 402 | } |
270 | 403 | return new Promise((resolve, reject) => { |
271 | 404 | this.resolveInput = (value) => { |
| 405 | + if (value !== ""){ |
| 406 | + this.history.push(value.slice(0, -1)); |
| 407 | + this.historyIndex = -1; |
| 408 | + this.cursorPosition = 0; |
| 409 | + } |
272 | 410 | resolve(value); |
273 | 411 | }; |
274 | 412 | }); |
|
362 | 500 |
|
363 | 501 | runButton.addEventListener("click", (e) => { |
364 | 502 | terminal.clear(); |
| 503 | + terminal.reset(); // reset the history |
365 | 504 | programRunning(true); |
366 | 505 | const code = codeBox.value; |
367 | 506 | pythonWorkerManager.run({ |
|
372 | 511 |
|
373 | 512 | replButton.addEventListener("click", (e) => { |
374 | 513 | terminal.clear(); |
| 514 | + terminal.reset(); // reset the history |
375 | 515 | programRunning(true); |
376 | 516 | // Need to use "-i -" to force interactive mode. |
377 | 517 | // Looks like isatty always returns false in emscripten |
|
0 commit comments