|
100 | 100 | class WasmTerminal {
|
101 | 101 |
|
102 | 102 | constructor() {
|
| 103 | + this.inputBuffer = new BufferQueue(); |
103 | 104 | this.input = ''
|
104 | 105 | this.resolveInput = null
|
105 | 106 | this.activeInput = false
|
|
123 | 124 | this.xterm.open(container);
|
124 | 125 | }
|
125 | 126 |
|
126 |
| - handleReadComplete(lastChar) { |
127 |
| - this.resolveInput(this.input + lastChar) |
128 |
| - this.activeInput = false |
129 |
| - } |
130 |
| - |
131 | 127 | handleTermData = (data) => {
|
132 |
| - if (!this.activeInput) { |
133 |
| - return |
134 |
| - } |
135 | 128 | const ord = data.charCodeAt(0);
|
136 |
| - let ofs; |
| 129 | + data = data.replace(/\r(?!\n)/g, "\n") // Convert lone CRs to LF |
137 | 130 |
|
| 131 | + // Handle pasted data |
| 132 | + if (data.length > 1 && data.includes("\n")) { |
| 133 | + let alreadyWrittenChars = 0; |
| 134 | + // If line already had data on it, merge pasted data with it |
| 135 | + if (this.input != '') { |
| 136 | + this.inputBuffer.addData(this.input); |
| 137 | + alreadyWrittenChars = this.input.length; |
| 138 | + this.input = ''; |
| 139 | + } |
| 140 | + this.inputBuffer.addData(data); |
| 141 | + // If input is active, write the first line |
| 142 | + if (this.activeInput) { |
| 143 | + let line = this.inputBuffer.nextLine(); |
| 144 | + this.writeLine(line.slice(alreadyWrittenChars)); |
| 145 | + this.resolveInput(line); |
| 146 | + this.activeInput = false; |
| 147 | + } |
| 148 | + // When input isn't active, add to line buffer |
| 149 | + } else if (!this.activeInput) { |
| 150 | + // Skip non-printable characters |
| 151 | + if (!(ord === 0x1b || ord == 0x7f || ord < 32)) { |
| 152 | + this.inputBuffer.addData(data); |
| 153 | + } |
138 | 154 | // TODO: Handle ANSI escape sequences
|
139 |
| - if (ord === 0x1b) { |
| 155 | + } else if (ord === 0x1b) { |
140 | 156 | // Handle special characters
|
141 | 157 | } else if (ord < 32 || ord === 0x7f) {
|
142 | 158 | switch (data) {
|
143 |
| - case "\r": // ENTER |
| 159 | + case "\x0c": // CTRL+L |
| 160 | + this.clear(); |
| 161 | + break; |
| 162 | + case "\n": // ENTER |
144 | 163 | case "\x0a": // CTRL+J
|
145 | 164 | case "\x0d": // CTRL+M
|
146 |
| - this.xterm.write('\r\n'); |
147 |
| - this.handleReadComplete('\n'); |
| 165 | + this.resolveInput(this.input + this.writeLine('\n')); |
| 166 | + this.input = ''; |
| 167 | + this.activeInput = false; |
148 | 168 | break;
|
149 | 169 | case "\x7F": // BACKSPACE
|
150 | 170 | case "\x08": // CTRL+H
|
|
157 | 177 | }
|
158 | 178 | }
|
159 | 179 |
|
| 180 | + writeLine(line) { |
| 181 | + this.xterm.write(line.slice(0, -1)) |
| 182 | + this.xterm.write('\r\n'); |
| 183 | + return line; |
| 184 | + } |
| 185 | + |
160 | 186 | handleCursorInsert(data) {
|
161 | 187 | this.input += data;
|
162 | 188 | this.xterm.write(data)
|
|
176 | 202 | this.activeInput = true
|
177 | 203 | // Hack to allow stdout/stderr to finish before we figure out where input starts
|
178 | 204 | setTimeout(() => {this.inputStartCursor = this.xterm.buffer.active.cursorX}, 1)
|
| 205 | + // If line buffer has a line ready, send it immediately |
| 206 | + if (this.inputBuffer.hasLineReady()) { |
| 207 | + return new Promise((resolve, reject) => { |
| 208 | + resolve(this.writeLine(this.inputBuffer.nextLine())); |
| 209 | + this.activeInput = false; |
| 210 | + }) |
| 211 | + // If line buffer has an incomplete line, use it for the active line |
| 212 | + } else if (this.inputBuffer.lastLineIsIncomplete()) { |
| 213 | + // Hack to ensure cursor input start doesn't end up after user input |
| 214 | + setTimeout(() => {this.handleCursorInsert(this.inputBuffer.nextLine())}, 1); |
| 215 | + } |
179 | 216 | return new Promise((resolve, reject) => {
|
180 | 217 | this.resolveInput = (value) => {
|
181 |
| - this.input = '' |
182 | 218 | resolve(value)
|
183 | 219 | }
|
184 | 220 | })
|
|
188 | 224 | this.xterm.clear();
|
189 | 225 | }
|
190 | 226 |
|
191 |
| - print(message) { |
192 |
| - const normInput = message.replace(/[\r\n]+/g, "\n").replace(/\n/g, "\r\n"); |
193 |
| - this.xterm.write(normInput); |
| 227 | + print(charCode) { |
| 228 | + let array = [charCode]; |
| 229 | + if (charCode == 10) { |
| 230 | + array = [13, 10]; // Replace \n with \r\n |
| 231 | + } |
| 232 | + this.xterm.write(new Uint8Array(array)); |
| 233 | + } |
| 234 | +} |
| 235 | + |
| 236 | +class BufferQueue { |
| 237 | + constructor(xterm) { |
| 238 | + this.buffer = [] |
| 239 | + } |
| 240 | + |
| 241 | + isEmpty() { |
| 242 | + return this.buffer.length == 0 |
| 243 | + } |
| 244 | + |
| 245 | + lastLineIsIncomplete() { |
| 246 | + return !this.isEmpty() && !this.buffer[this.buffer.length-1].endsWith("\n") |
| 247 | + } |
| 248 | + |
| 249 | + hasLineReady() { |
| 250 | + return !this.isEmpty() && this.buffer[0].endsWith("\n") |
| 251 | + } |
| 252 | + |
| 253 | + addData(data) { |
| 254 | + let lines = data.match(/.*(\n|$)/g) |
| 255 | + if (this.lastLineIsIncomplete()) { |
| 256 | + this.buffer[this.buffer.length-1] += lines.shift() |
| 257 | + } |
| 258 | + for (let line of lines) { |
| 259 | + this.buffer.push(line) |
| 260 | + } |
| 261 | + } |
| 262 | + |
| 263 | + nextLine() { |
| 264 | + return this.buffer.shift() |
194 | 265 | }
|
195 | 266 | }
|
196 | 267 |
|
|
202 | 273 | terminal.open(document.getElementById('terminal'))
|
203 | 274 |
|
204 | 275 | const stdio = {
|
205 |
| - stdout: (s) => { terminal.print(s) }, |
206 |
| - stderr: (s) => { terminal.print(s) }, |
| 276 | + stdout: (charCode) => { terminal.print(charCode) }, |
| 277 | + stderr: (charCode) => { terminal.print(charCode) }, |
207 | 278 | stdin: async () => {
|
208 | 279 | return await terminal.prompt()
|
209 | 280 | }
|
|
0 commit comments