Mercurial > hg > rlgwebd
comparison termemu.js @ 0:bd412f63ce0d
Put this project under version control, finally.
author | John "Elwin" Edwards <elwin@sdf.org> |
---|---|
date | Sun, 06 May 2012 08:45:40 -0700 |
parents | |
children | ee22eb9ab009 |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:bd412f63ce0d |
---|---|
1 /* termemu.js: a mostly xterm-compatible terminal emulator for a webpage */ | |
2 /* SELF-HOSTING 2011-09-23 */ | |
3 | |
4 // How detailed the debugging should be. | |
5 var debugSuppress = 1; | |
6 // Some char values. | |
7 var csiPre = [63, 62, 33]; | |
8 var csiPost = [36, 34, 39, 32]; | |
9 function csiFinal(code) { | |
10 /* @A-Z */ | |
11 if (code >= 64 && code <= 90) | |
12 return true; | |
13 /* `a-z{| */ | |
14 if (code >= 96 && code <= 124) | |
15 return true; | |
16 return false; | |
17 } | |
18 var esc7ctl = [68, 69, 72, 77, 78, 79, 80, 86, 87, 88, 90, 91, 92, 93, 94, 95]; | |
19 var escSingle = [55, 56, 61, 62, 70, 99, 108, 109, 110, 111, 124, 125, 126]; | |
20 var escDouble = [32, 35, 37, 40, 41, 42, 43, 45, 46, 47]; | |
21 | |
22 var decChars = {96: 0x2666, 97: 0x2592, 102: 0xB0, 103: 0xB1, | |
23 106: 0x2518, 107: 0x2510, 108: 0x250C, 109: 0x2514, | |
24 110: 0x253C, 111: 0x23BA, 112: 0x23BB, 113: 0x2500, | |
25 114: 0x23BC, 115: 0x23BD, 116: 0x251C, 117: 0x2524, | |
26 118: 0x2534, 119: 0x252C, 120: 0x2502, 121: 0x2264, | |
27 122: 0x2265, 123: 0x03C0, 124: 0x2260, 125: 0xA3, 126: 0xB7}; | |
28 | |
29 /* Not everything that should be saved by DECSC has been implemented yet. */ | |
30 function Cursor(src) { | |
31 if (src) { | |
32 this.x = src.x; | |
33 this.y = src.y; | |
34 this.bold = src.bold; | |
35 this.inverse = src.inverse; | |
36 this.uline = src.uline; | |
37 this.fg = src.fg; | |
38 this.bg = src.bg; | |
39 this.cset = src.cset; | |
40 } | |
41 else { | |
42 this.x = 0; | |
43 this.y = 0; | |
44 this.bold = false; | |
45 this.inverse = false; | |
46 this.uline = false; | |
47 this.fg = null; | |
48 this.bg = null; | |
49 this.cset = "B"; | |
50 } | |
51 return; | |
52 } | |
53 | |
54 // An object representing the terminal emulator. | |
55 var termemu = { | |
56 sessid: null, // Session key assigned by the server | |
57 /* Some elements of the page. */ | |
58 inwrap: null, // A non-table div wrapping the screen | |
59 view: null, // The div holding the terminal screen | |
60 screen: null, // The div representing the active screen area | |
61 normbuf: null, // The normal screen buffer | |
62 altbuf: null, // The alternate screen buffer | |
63 histbuf: null, // The screen history buffer | |
64 fgColor: "#b2b2b2", // Default color for text | |
65 bgColor: "black", // Default background color | |
66 c: null, // Contains cursor position and text attributes | |
67 offedge: false, // Going off the edge doesn't mean adding a new line | |
68 clearAttrs: function () { | |
69 /* Make sure to reset ALL attribute properties and NOTHING else. */ | |
70 this.c.bold = false; | |
71 this.c.inverse = false; | |
72 this.c.uline = false; | |
73 this.c.fg = null; | |
74 this.c.bg = null; | |
75 }, | |
76 saved: null, // saved cursor | |
77 normc: null, // Stores the normal screen buffer cursor when using altbuf | |
78 ansicolors: ["#000000", "#b21818", "#18b218", "#b26818", "#1818b2", | |
79 "#b218b2", "#18b2b2", "#b2b2b2"], | |
80 brightcolors: ["#686868", "#ff5454", "#54ff54", "#ffff54", "#5454ff", | |
81 "#ff54ff", "#54ffff", "#ffffff"], | |
82 cssColor: function (fg) { | |
83 /* returns a CSS color specification for the text or background */ | |
84 var n; | |
85 var fallback; | |
86 var cube6 = ["00", "5f", "87", "af", "d7", "ff"]; | |
87 if (this.c.inverse) | |
88 fg = !fg; | |
89 if (fg) { | |
90 n = this.c.fg; | |
91 fallback = this.fgColor; | |
92 if (n == null) | |
93 return fallback; | |
94 } | |
95 else { | |
96 n = this.c.bg; | |
97 fallback = this.bgColor; | |
98 if (n == null) | |
99 return fallback; | |
100 } | |
101 if (n < 0) | |
102 return fallback; | |
103 else if (n < 8) { | |
104 if (this.c.bold && fg) | |
105 return this.brightcolors[n]; | |
106 else | |
107 return this.ansicolors[n]; | |
108 } | |
109 else if (n < 16) | |
110 return this.brightcolors[n - 8]; | |
111 else if (n < 232) { | |
112 var r = cube6[Math.floor((n - 16) / 36)]; | |
113 var g = cube6[Math.floor((n - 16) / 6) % 6]; | |
114 var b = cube6[(n - 16) % 6]; | |
115 return "#" + r + g + b; | |
116 } | |
117 else if (n < 256) { | |
118 var colstr = ((n - 232) * 10 + 8).toString(16); | |
119 if (colstr.length < 2) | |
120 colstr = "0" + colstr; | |
121 return "#" + colstr + colstr + colstr; | |
122 } | |
123 else | |
124 return fallback; | |
125 }, | |
126 scrT: 0, // top and bottom of scrolling region | |
127 scrB: 23, | |
128 // These keyboard-related things don't really belong here. | |
129 shift: false, | |
130 shiftp: function () { | |
131 return this.shift; | |
132 }, | |
133 toggleshift: function () { | |
134 this.shift = !this.shift; | |
135 }, | |
136 ctrl: false, | |
137 ctrlp: function () { | |
138 return this.ctrl; | |
139 }, | |
140 togglectrl: function () { | |
141 this.ctrl = !this.ctrl; | |
142 }, | |
143 init: function (divID) { | |
144 /* Makes a div full of character cells. */ | |
145 if (this.screen != null) | |
146 return; | |
147 var owrap = document.getElementById(divID); | |
148 if (!owrap) | |
149 return; | |
150 while (owrap.firstChild != null) | |
151 owrap.removeChild(owrap.firstChild); | |
152 this.c = new Cursor(null); | |
153 /* Create the contents of the terminal div */ | |
154 this.inwrap = document.createElement("div"); | |
155 this.inwrap.id = "inwrap"; | |
156 owrap.appendChild(this.inwrap); | |
157 var termdiv = document.createElement("div"); | |
158 termdiv.id = "term"; | |
159 termdiv.style.fontSize = "12px"; | |
160 this.inwrap.appendChild(termdiv); | |
161 /* Set up the screen buffers */ | |
162 this.histbuf = document.createElement("div"); | |
163 this.histbuf.id = "histbuf"; | |
164 termdiv.appendChild(this.histbuf); | |
165 this.normbuf = document.createElement("div"); | |
166 this.normbuf.id = "normbuf"; | |
167 termdiv.appendChild(this.normbuf); | |
168 for (var row = 0; row < 24; row++) { | |
169 this.normbuf.appendChild(this.makeRow()); | |
170 } | |
171 this.altbuf = document.createElement("div"); | |
172 this.altbuf.id = "altbuf"; | |
173 termdiv.appendChild(this.altbuf); | |
174 this.altbuf.style.display = "none"; | |
175 /* altbuf will be filled when it is used. */ | |
176 /* Attach them. */ | |
177 this.view = termdiv; | |
178 this.screen = this.normbuf; | |
179 this.resize(); | |
180 this.cmove(0, 0); | |
181 }, | |
182 valign: function () { | |
183 if (this.screen == this.normbuf) | |
184 this.inwrap.scrollTop = this.histbuf.clientHeight; | |
185 }, | |
186 resize: function () { | |
187 var owrap = document.getElementById("termwrap"); | |
188 /* Set the height up properly. */ | |
189 this.inwrap.style.height = this.screen.scrollHeight.toString() + "px"; | |
190 this.valign(); | |
191 // Figure out how wide the vertical scrollbar is. | |
192 var dwid = this.inwrap.offsetWidth - this.inwrap.clientWidth; | |
193 // And resize accordingly. | |
194 this.inwrap.style.width = (this.view.scrollWidth + dwid).toString() + "px"; | |
195 owrap.style.width = this.inwrap.offsetWidth.toString() + "px"; | |
196 return; | |
197 }, | |
198 comseq: [], // Part of an impending control sequence | |
199 flipCursor: function () { | |
200 /* Swaps the text and background colors of the active location. */ | |
201 /* This will change when other cursor styles are supported. */ | |
202 if (this.c.x != null && this.c.y != null) { | |
203 var oldcell = this.screen.childNodes[this.c.y].childNodes[this.c.x]; | |
204 var tempswap = oldcell.style.color; | |
205 oldcell.style.color = oldcell.style.backgroundColor; | |
206 oldcell.style.backgroundColor = tempswap; | |
207 } | |
208 return; | |
209 }, | |
210 saveCursor: function () { | |
211 this.saved = new Cursor(this.c); | |
212 return; | |
213 }, | |
214 restoreCursor: function () { | |
215 if (!this.saved) { | |
216 this.cmove(0, 0); | |
217 this.c = new Cursor(null); | |
218 } | |
219 else { | |
220 this.cmove(this.saved.y, this.saved.x); | |
221 this.c = new Cursor(this.saved); | |
222 } | |
223 return; | |
224 }, | |
225 toAltBuf: function () { | |
226 if (this.screen == this.altbuf) | |
227 return; | |
228 while (this.altbuf.firstChild != null) | |
229 this.altbuf.removeChild(this.altbuf.firstChild); | |
230 for (var i = 0; i < 24; i++) { | |
231 this.altbuf.appendChild(this.makeRow()); | |
232 } | |
233 this.normc = new Cursor(this.c); | |
234 this.altbuf.style.display = "table-row-group"; | |
235 this.normbuf.style.display = "none"; | |
236 this.histbuf.style.display = "none"; | |
237 this.screen = this.altbuf; | |
238 debug(0, "Altbuf with charset " + this.c.cset); | |
239 return; | |
240 }, | |
241 toNormBuf: function () { | |
242 if (this.screen == this.normbuf) | |
243 return; | |
244 this.altbuf.style.display = "none"; | |
245 this.normbuf.style.display = "table-row-group"; | |
246 this.histbuf.style.display = "table-row-group"; | |
247 this.screen = this.normbuf; | |
248 this.valign(); | |
249 /* The cursor isn't actually at this position in normbuf, but cmove will | |
250 * flip it anyway. Flip it again to compensate. */ | |
251 this.flipCursor(); | |
252 this.cmove(this.normc.y, this.normc.x); | |
253 this.c = new Cursor(this.normc); | |
254 }, | |
255 cmove: function (y, x) { | |
256 /* Move the cursor. NOTE coords are [row, col] as in curses. */ | |
257 /* If x or y is null, that coordinate is not altered. */ | |
258 /* Sanity checks and initializations. */ | |
259 if (x == null) { | |
260 if (this.c.x != null) | |
261 x = this.c.x; | |
262 else | |
263 return; | |
264 } | |
265 else { | |
266 this.offedge = false; | |
267 if (x < 0) | |
268 x = 0; | |
269 else if (x > 79) | |
270 x = 79; | |
271 } | |
272 if (y == null) { | |
273 if (this.c.y != null) | |
274 y = this.c.y; | |
275 else | |
276 return; | |
277 } | |
278 else if (y < 0) | |
279 y = 0; | |
280 else if (y > 23) | |
281 y = 23; | |
282 /* Un-reverse video the current location. */ | |
283 this.flipCursor(); | |
284 this.c.x = x; | |
285 this.c.y = y; | |
286 /* Reverse-video the new location. */ | |
287 this.flipCursor(); | |
288 return; | |
289 }, | |
290 historize: function (n) { | |
291 if (n < 0 || n >= this.screen.childNodes.length) | |
292 return; | |
293 var oldrow = this.screen.childNodes[n]; | |
294 if (this.screen != this.altbuf && this.scrT == 0) { | |
295 this.histbuf.appendChild(oldrow); | |
296 } | |
297 else { | |
298 this.screen.removeChild(oldrow); | |
299 } | |
300 /* These may not be the correct heights... */ | |
301 this.inwrap.style.height = this.screen.clientHeight.toString() + "px"; | |
302 this.valign(); | |
303 }, | |
304 scroll: function (lines) { | |
305 if (lines == 0) | |
306 return; | |
307 var count; | |
308 if (lines > 0) | |
309 count = lines; | |
310 else | |
311 count = -lines; | |
312 this.flipCursor(); | |
313 while (count > 0) { | |
314 var blankrow = this.makeRow(); | |
315 /* Careful with the order */ | |
316 if (lines > 0) { | |
317 if (this.scrB == 23) | |
318 this.screen.appendChild(blankrow); | |
319 else | |
320 this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB | |
321 + 1]); | |
322 this.historize(this.scrT); | |
323 } | |
324 else { | |
325 /* Historize here? */ | |
326 this.screen.removeChild(this.screen.childNodes[this.scrB]); | |
327 this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrT]); | |
328 } | |
329 count--; | |
330 } | |
331 this.valign(); // needed? | |
332 this.flipCursor(); | |
333 return; | |
334 }, | |
335 newline: function (doReturn) { | |
336 if (this.c.y == this.scrB) | |
337 this.scroll(1) | |
338 else if (this.c.y < 23) | |
339 this.cmove(this.c.y + 1, null); | |
340 /* If the cursor is at the bottom but outside the scrolling region, | |
341 * nothing can be done. */ | |
342 if (doReturn) { | |
343 this.cmove(null, 0); | |
344 } | |
345 }, | |
346 antinewline: function () { | |
347 if (this.c.y == this.scrT) | |
348 this.scroll(-1); | |
349 else if (this.c.y > 0) | |
350 this.cmove(this.c.y - 1, null); | |
351 }, | |
352 advance: function () { | |
353 if (this.c.x < 79) | |
354 this.cmove(null, this.c.x + 1); | |
355 else { | |
356 this.offedge = true; | |
357 } | |
358 }, | |
359 placechar: function (str) { | |
360 if (this.offedge) { | |
361 this.newline(true); | |
362 } | |
363 var nextch = str.charAt(0); | |
364 var newcell = this.makeCell(nextch); | |
365 var rowdiv = this.screen.childNodes[this.c.y]; | |
366 rowdiv.replaceChild(newcell, rowdiv.childNodes[this.c.x]); | |
367 this.flipCursor(); // The replace removed the cursor. | |
368 /* Update the cursor. */ | |
369 this.advance(); | |
370 }, | |
371 reset: function () { | |
372 /* Reset ALL state, hopefully in the right order. */ | |
373 /* TODO test this and compare it with xterm */ | |
374 this.toNormBuf(); | |
375 this.clearAttrs(); | |
376 this.c.cset = 'B'; | |
377 this.cmove(0, 0); | |
378 this.saved = null; | |
379 this.normc = null; | |
380 this.scrT = 0; | |
381 this.scrB = 23; | |
382 while (this.histbuf.firstChild != null) { | |
383 this.histbuf.removeChild(this.histbuf.firstChild); | |
384 } | |
385 for (var i = 0; i < 24; i++) { | |
386 this.screen.replaceChild(this.makeRow(), this.screen.childNodes[i]); | |
387 } | |
388 this.flipCursor(); // make it appear in the new row | |
389 return; | |
390 }, | |
391 write: function (codes) { | |
392 //dchunk(codes); | |
393 for (var i = 0; i < codes.length; i++) { | |
394 /* First see if there's an incomplete command sequence waiting. */ | |
395 if (this.comseq.length > 0) { | |
396 if (this.comseq.length == 1 && this.comseq[0] == 27) { | |
397 /* Just ESC */ | |
398 if (codes[i] == 55) { | |
399 /* ESC 7 : save cursor */ | |
400 this.saveCursor(); | |
401 this.comseq = []; | |
402 } | |
403 else if (codes[i] == 56) { | |
404 /* ESC 8 : restore cursor */ | |
405 this.restoreCursor(); | |
406 this.comseq = []; | |
407 } | |
408 else if (codes[i] == 61) { | |
409 /* ESC = : application keypad */ | |
410 keyHexCodes.appKeypad(true); | |
411 this.comseq = []; | |
412 } | |
413 else if (codes[i] == 62) { | |
414 /* ESC > : normal keypad */ | |
415 keyHexCodes.appKeypad(false); | |
416 this.comseq = []; | |
417 } | |
418 else if (codes[i] == 68) { | |
419 /* ESC D = IND */ | |
420 this.newline(false); | |
421 this.comseq = []; | |
422 } | |
423 else if (codes[i] == 69) { | |
424 /* ESC E = NEL */ | |
425 this.newline(true); | |
426 this.comseq = []; | |
427 } | |
428 else if (codes[i] == 77) { | |
429 /* ESC M = RI */ | |
430 this.antinewline(); | |
431 this.comseq = []; | |
432 } | |
433 else if (codes[i] == 91) { | |
434 /* ESC [ = CSI */ | |
435 this.comseq[0] = 155; | |
436 } | |
437 else if (codes[i] == 93) { | |
438 /* ESC [ = OSC */ | |
439 this.comseq[0] = 157; | |
440 } | |
441 else if (codes[i] == 99) { | |
442 /* ESC c = reset */ | |
443 this.reset(); | |
444 this.comseq = []; | |
445 } | |
446 else if (escSingle.indexOf(codes[i]) >= 0) { | |
447 /* An unimplemented two-char sequence. */ | |
448 debug(1, "Unimplemented sequence ESC " + codes[i].toString(16)); | |
449 this.comseq = []; | |
450 } | |
451 else if (escDouble.indexOf(codes[i]) >= 0) { | |
452 /* A three-char sequence. */ | |
453 this.comseq.push(codes[i]); | |
454 } | |
455 else { | |
456 /* Nothing else is implemented yet. */ | |
457 debug(1, "Unrecognized sequence ESC " + codes[i].toString(16)); | |
458 this.comseq = []; | |
459 } | |
460 } | |
461 else if (this.comseq.length == 2 && this.comseq[0] == 27) { | |
462 /* An ESC C N sequence. Not implemented. Doesn't check validity | |
463 * of N. */ | |
464 if (this.comseq[1] == 40) { | |
465 if (codes[i] == 48) { | |
466 this.c.cset = "0"; | |
467 debug(0, "Switching to DEC graphics."); | |
468 } | |
469 else if (codes[i] == 66) { | |
470 this.c.cset = "B"; | |
471 debug(0, "Switching to ASCII."); | |
472 } | |
473 else { | |
474 debug(1, "Unimplemented character set: " + | |
475 String.fromCharCode(codes[i])); | |
476 } | |
477 debug(0, "cset is now: " + this.c.cset); | |
478 } | |
479 else | |
480 debug(1, "Unknown sequence ESC " + | |
481 String.fromCharCode(this.comseq[1]) + " 0x" + | |
482 codes[i].toString(16)); | |
483 this.comseq = []; | |
484 } | |
485 else if (this.comseq[0] == 157) { | |
486 /* Commands beginning with OSC */ | |
487 /* Check for string terminator */ | |
488 if (codes[i] == 7 || codes[i] == 156 || (codes[i] == 92 && | |
489 this.comseq[this.comseq.length - 1] == 27)) { | |
490 if (codes[i] == 92 && this.comseq[this.comseq.length - 1] == 27) | |
491 this.comseq.pop(); | |
492 debug(0, "Got " + (this.comseq.length - 1) + "-byte OSC sequence"); | |
493 this.oscProcess(); | |
494 this.comseq = []; | |
495 } | |
496 else | |
497 this.comseq.push(codes[i]); | |
498 } | |
499 else if (this.comseq[0] == 155) { | |
500 /* Commands starting with CSI */ | |
501 // End at first char that's not numeric ; ? > ! $ " space ' | |
502 // i.e. letter @ ` lbrace | | |
503 // ?>! must come directly after CSI | |
504 // $"'space must come directly before terminator | |
505 // FIXME put this checking code into csiProcess | |
506 if (csiPre.indexOf(codes[i]) >= 0) { | |
507 if (this.comseq.length > 1) { | |
508 /* Chars in csiPre can only occur right after the CSI */ | |
509 debug(1, "Invalid CSI sequence: misplaced prefix"); | |
510 this.comseq = []; | |
511 } | |
512 else | |
513 this.comseq.push(codes[i]); | |
514 } | |
515 else if (csiPost.indexOf(this.comseq[this.comseq.length - 1]) >= 0 && | |
516 !csiFinal(codes[i])) { | |
517 /* Chars is csiPost must come right before the final char */ | |
518 debug(1, "Invalid CSI sequence: misplaced postfix"); | |
519 this.comseq = []; | |
520 } | |
521 else if ((codes[i] >= 48 && codes[i] <= 57) || codes[i] == 59 || | |
522 csiPost.indexOf(codes[i]) >= 0) { | |
523 /* Numbers and ; can go anywhere */ | |
524 this.comseq.push(codes[i]); | |
525 } | |
526 else if (csiFinal(codes[i])) { | |
527 this.comseq.push(codes[i]); | |
528 this.csiProcess(); | |
529 this.comseq = []; | |
530 } | |
531 else { | |
532 debug(1, "Invalid CSI sequence: unknown code " + codes[i].toString(16)); | |
533 this.comseq = []; | |
534 } | |
535 } | |
536 else { | |
537 debug(1, "Unknown sequence with " + this.comseq[0].toString(16)); | |
538 this.comseq = []; | |
539 } | |
540 continue; | |
541 } | |
542 /* Treat it as a single character. */ | |
543 if (codes[i] == 5) { | |
544 sendback("06"); | |
545 } | |
546 else if (codes[i] == 7) { | |
547 /* bell */ | |
548 bell(true); | |
549 } | |
550 else if (codes[i] == 8) { | |
551 /* backspace */ | |
552 if (this.offedge) | |
553 this.offedge = false; | |
554 else if (this.c.x > 0) | |
555 this.cmove(null, this.c.x - 1); | |
556 } | |
557 else if (codes[i] == 9) { | |
558 /* tab */ | |
559 var xnew; | |
560 if (this.c.x < 79) { | |
561 xnew = 8 * (Math.floor(this.c.x / 8) + 1); | |
562 if (xnew > 79) | |
563 xnew = 79; | |
564 this.cmove(null, xnew); | |
565 } | |
566 else { | |
567 this.offedge = true; | |
568 } | |
569 } | |
570 else if (codes[i] >= 10 && codes[i] <= 12) { | |
571 /* newline, vertical tab, form feed */ | |
572 if (this.offedge) | |
573 this.newline(true); | |
574 else | |
575 this.newline(false); | |
576 } | |
577 else if (codes[i] == 13) { | |
578 /* carriage return \r */ | |
579 this.cmove(null, 0); | |
580 } | |
581 else if (codes[i] == 14) { | |
582 /* shift out */ | |
583 // Currently assuming that G1 is DEC Special & Line Drawing | |
584 this.c.cset = "0"; | |
585 debug(0, "Using DEC graphics charset."); | |
586 } | |
587 else if (codes[i] == 15) { | |
588 /* shift in */ | |
589 // Currently assuming that G0 is ASCII | |
590 this.c.cset = "B"; | |
591 debug(0, "Using ASCII charset."); | |
592 } | |
593 else if (codes[i] == 27) { | |
594 /* escape */ | |
595 this.comseq.push(codes[i]); | |
596 } | |
597 else if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160)) { | |
598 /* Some kind of control character. */ | |
599 debug(1, "Unprintable character 0x" + codes[i].toString(16)); | |
600 } | |
601 else { | |
602 /* If it's ASCII, it's printable; take a risk on anything higher */ | |
603 if ((this.c.cset == "0") && (codes[i] in decChars)) { | |
604 // DEC special character set | |
605 this.placechar(String.fromCharCode(decChars[codes[i]])); | |
606 } | |
607 else { | |
608 this.placechar(String.fromCharCode(codes[i])); | |
609 } | |
610 } | |
611 } | |
612 return; | |
613 }, | |
614 csiProcess: function () { | |
615 /* Processes the CSI sequence in this.comseq */ | |
616 var c = this.comseq[this.comseq.length - 1]; | |
617 if (this.comseq[0] != 155 || !csiFinal(c)) | |
618 return; | |
619 var comstr = ""; | |
620 for (var i = 1; i < this.comseq.length; i++) | |
621 comstr += String.fromCharCode(this.comseq[i]); | |
622 debug(0, "CSI sequence: " + comstr); | |
623 var reCSI = /^([>?!])?([0-9;]*)([ "$'])?([A-Za-z@`{|])$/; | |
624 var matchCSI = comstr.match(reCSI); | |
625 if (!matchCSI) { | |
626 debug(1, "Unrecognized CSI sequence: " + comstr); | |
627 return; | |
628 } | |
629 var prefix = null; | |
630 if (matchCSI[1]) | |
631 prefix = matchCSI[1]; | |
632 var postfix = null; | |
633 if (matchCSI[3]) | |
634 postfix = matchCSI[3]; | |
635 var params = []; | |
636 if (matchCSI[2]) { | |
637 var numstrs = matchCSI[2].split(";"); | |
638 for (var i = 0; i < numstrs.length; i++) { | |
639 if (numstrs[i]) | |
640 params.push(Number(numstrs[i])); | |
641 else | |
642 params.push(null); | |
643 } | |
644 } | |
645 /* Useful if expecting a single parameter which is a count. */ | |
646 var count = 1; | |
647 if (params[0]) | |
648 count = params[0]; | |
649 /* The final character determines the action. */ | |
650 if (c == 64) { | |
651 /* @ - insert spaces at cursor */ | |
652 if (prefix || postfix) { | |
653 debug(1, "Invalid CSI @ sequence: " + comstr); | |
654 return; | |
655 } | |
656 /* The cursor stays still, but characters move out from under it. */ | |
657 this.flipCursor(); | |
658 var rowdiv = this.screen.childNodes[this.c.y]; | |
659 var newspace; | |
660 while (count > 0) { | |
661 newspace = this.makeCell(' '); | |
662 rowdiv.insertBefore(newspace, rowdiv.childNodes[this.c.x]); | |
663 rowdiv.removeChild(rowdiv.lastChild); | |
664 count--; | |
665 } | |
666 /* Finally, put the cursor back. */ | |
667 this.flipCursor(); | |
668 } | |
669 else if (c >= 65 && c <= 71) { | |
670 /* A - up, B - down, C - forward, D - backward */ | |
671 /* E - next line, F - previous line, G - to column */ | |
672 if (prefix || postfix) { | |
673 debug(1, "Invalid CSI sequence: " + comstr); | |
674 return; | |
675 } | |
676 /* These may be out of range, but cmove will take care of that. */ | |
677 if (c == 65) | |
678 this.cmove(this.c.y - count, null); | |
679 else if (c == 66) | |
680 this.cmove(this.c.y + count, null); | |
681 else if (c == 67) | |
682 this.cmove(null, this.c.x + count); | |
683 else if (c == 68) | |
684 this.cmove(null, this.c.x - count); | |
685 else if (c == 69) | |
686 this.cmove(this.c.y + count, 0); | |
687 else if (c == 70) | |
688 this.cmove(this.c.y - count, 0); | |
689 else if (c == 71) | |
690 this.cmove(null, count - 1); | |
691 } | |
692 else if (c == 72) { | |
693 /* H - move */ | |
694 var x = 0; | |
695 var y = 0; | |
696 if (prefix || postfix) { | |
697 debug(1, "Invalid CSI H sequence: " + comstr); | |
698 return; | |
699 } | |
700 if (params[0]) | |
701 y = params[0] - 1; | |
702 if (params[1]) | |
703 x = params[1] - 1; | |
704 if (y > 23) | |
705 y = 23; | |
706 if (x > 79) | |
707 x = 79; | |
708 debug(0, "Moving to row " + y + ", col " + x); | |
709 this.cmove(y, x); | |
710 } | |
711 else if (c == 73) { | |
712 /* I - move forward by tabs */ | |
713 var x = this.c.x; | |
714 if (prefix || postfix) { | |
715 debug(1, "Invalid CSI I sequence: " + comstr); | |
716 return; | |
717 } | |
718 while (count > 0) { | |
719 x = 8 * (Math.floor(x / 8) + 1); | |
720 if (x > 79) { | |
721 x = 79; | |
722 break; | |
723 } | |
724 count--; | |
725 } | |
726 this.cmove(null, x); | |
727 } | |
728 else if (c == 74) { | |
729 /* J - erase display */ | |
730 var start; | |
731 var end; | |
732 var cols; | |
733 if (prefix == '?') | |
734 debug(1, "Warning: CSI ?J not implemented"); | |
735 else if (prefix || postfix) { | |
736 debug(1, "Invalid CSI J sequence: " + comstr); | |
737 return; | |
738 } | |
739 if (!params[0]) { | |
740 /* Either 0 or not given */ | |
741 start = this.c.y + 1; | |
742 end = 23; | |
743 cols = 1; | |
744 } | |
745 else if (params[0] == 1) { | |
746 start = 0; | |
747 end = this.c.y - 1; | |
748 cols = -1; | |
749 } | |
750 else if (params[0] == 2) { | |
751 start = 0; | |
752 end = 23; | |
753 cols = 0; | |
754 } | |
755 else { | |
756 debug(1, "Unimplemented parameter in CSI J sequence: " + comstr); | |
757 return; | |
758 } | |
759 for (var nrow = start; nrow <= end; nrow++) { | |
760 this.screen.replaceChild(this.makeRow(), this.screen.childNodes[nrow]); | |
761 } | |
762 if (cols != 0) { | |
763 /* Otherwise, the whole screen was erased and the active row doesn't | |
764 * need special treatment. */ | |
765 var cursrow = this.screen.childNodes[this.c.y]; | |
766 for (var ncol = this.c.x; ncol >= 0 && ncol < 80; ncol += cols) { | |
767 cursrow.replaceChild(this.makeCell(' '), cursrow.childNodes[ncol]); | |
768 } | |
769 } | |
770 this.offedge = false; | |
771 /* Always flip after replacing the active position. */ | |
772 this.flipCursor(); | |
773 } | |
774 else if (c == 75) { | |
775 /* K - erase line */ | |
776 /* The ? is for an erase method that respects an "uneraseable" attribute, | |
777 * which isn't implemented, so the methods are equivalent for now. */ | |
778 var start; | |
779 var end; | |
780 if (prefix == '?') | |
781 debug(1, "Warning: CSI ?K not implemented"); | |
782 else if (prefix || postfix) { | |
783 debug(1, "Invalid CSI K sequence: " + comstr); | |
784 return; | |
785 } | |
786 /* 0 (default): right, 1: left, 2: all. Include cursor position. */ | |
787 if (params[0] == 1) { | |
788 start = 0; | |
789 end = this.c.x; | |
790 } | |
791 else if (params[0] == 2) { | |
792 start = 0; | |
793 end = 79; | |
794 } | |
795 else { | |
796 start = this.c.x; | |
797 end = 79; | |
798 } | |
799 var rowdiv = this.screen.childNodes[this.c.y]; | |
800 for (var i = start; i <= end; i++) { | |
801 rowdiv.replaceChild(this.makeCell(' '), rowdiv.childNodes[i]); | |
802 } | |
803 /* Deleting stuff tends to clear this */ | |
804 this.offedge = false; | |
805 /* The active position is always cleared, so the cursor must be made | |
806 * visible again. */ | |
807 this.flipCursor(); | |
808 } | |
809 else if (c == 76 || c == 77) { | |
810 /* L - insert lines at the current position. | |
811 * M - delete current lines */ | |
812 if (prefix || postfix) { | |
813 debug(1, "Invalid CSI sequence: " + comstr); | |
814 return; | |
815 } | |
816 /* CSI LM have no effect outside of the scrolling region */ | |
817 if (this.c.y < this.scrT || this.c.y > this.scrB) | |
818 return; | |
819 this.flipCursor(); | |
820 while (count > 0) { | |
821 var blankrow = this.makeRow(); | |
822 if (c == 76) { | |
823 this.historize(this.scrB); | |
824 this.screen.insertBefore(blankrow, this.screen.childNodes[this.c.y]); | |
825 } | |
826 else { | |
827 if (this.scrB == 23) | |
828 this.screen.appendChild(blankrow); | |
829 else | |
830 this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB | |
831 + 1]); | |
832 this.historize(this.c.y); | |
833 } | |
834 count--; | |
835 } | |
836 /* It doesn't seem necessary to reset this, but xterm does it. */ | |
837 this.offedge = false; | |
838 this.flipCursor(); | |
839 } | |
840 else if (c == 80) { | |
841 /* P - delete at active position, causing cells on the right to shift. */ | |
842 if (prefix || postfix) { | |
843 debug(1, "Invalid CSI P sequence: " + comstr); | |
844 return; | |
845 } | |
846 var cursrow = this.screen.childNodes[this.c.y]; | |
847 while (count > 0) { | |
848 cursrow.removeChild(cursrow.childNodes[this.c.x]); | |
849 cursrow.appendChild(this.makeCell(' ')); | |
850 count--; | |
851 } | |
852 this.offedge = false; | |
853 this.flipCursor(); | |
854 } | |
855 else if (c == 83 || c == 84) { | |
856 /* S - scroll up, T - scroll down */ | |
857 if (prefix || postfix) { | |
858 debug(1, "Invalid CSI sequence: " + comstr); | |
859 return; | |
860 } | |
861 if (c == 83) | |
862 this.scroll(count); | |
863 else | |
864 this.scroll(-count); | |
865 } | |
866 else if (c == 88) { | |
867 /* X - erase characters */ | |
868 if (prefix || postfix) { | |
869 debug(1, "Invalid CSI sequence: " + comstr); | |
870 return; | |
871 } | |
872 var row = this.screen.childNodes[this.c.y]; | |
873 for (var i = 0; i < count && this.c.x + i < 80; i++) { | |
874 row.replaceChild(this.makeCell(' '), row.childNodes[this.c.x + i]); | |
875 } | |
876 this.flipCursor(); | |
877 } | |
878 else if (c == 90) { | |
879 /* Z - tab backwards */ | |
880 var x = this.c.x; | |
881 if (prefix || postfix) { | |
882 debug(1, "Invalid CSI Z sequence: " + comstr); | |
883 return; | |
884 } | |
885 while (count > 0) { | |
886 x = 8 * (Math.ceil(x / 8) - 1); | |
887 if (x < 0) { | |
888 x = 0; | |
889 break; | |
890 } | |
891 count--; | |
892 } | |
893 this.cmove(null, x); | |
894 } | |
895 else if (c == 96) { | |
896 /* ` - go to col */ | |
897 if (prefix || postfix) { | |
898 debug(1, "Invalid CSI ` sequence: " + comstr); | |
899 return; | |
900 } | |
901 this.cmove(null, count - 1); | |
902 } | |
903 else if (c == 100) { | |
904 /* d - go to row */ | |
905 if (prefix || postfix) { | |
906 debug(1, "Invalid CSI d sequence: " + comstr); | |
907 return; | |
908 } | |
909 this.cmove(count - 1, null); | |
910 } | |
911 else if (c == 102) { | |
912 /* f - move */ | |
913 var x = 0; | |
914 var y = 0; | |
915 if (prefix || postfix) { | |
916 debug(1, "Invalid CSI f sequence: " + comstr); | |
917 return; | |
918 } | |
919 if (params[0]) | |
920 y = params[0] - 1; | |
921 if (params[1]) | |
922 x = params[1] - 1; | |
923 this.cmove(y, x); | |
924 } | |
925 else if (c == 104) { | |
926 /* h - set modes */ | |
927 if (prefix != '?') { | |
928 debug(1, "Unimplemented CSI sequence: " + comstr); | |
929 return; | |
930 } | |
931 for (var i = 0; i < params.length; i++) { | |
932 if (params[i] == 1) { | |
933 keyHexCodes.appCursor(true); | |
934 } | |
935 else if (params[i] == 1048) { | |
936 this.saveCursor(); | |
937 } | |
938 else if (params[i] == 1049) { | |
939 this.toAltBuf(); | |
940 } | |
941 else { | |
942 debug(1, "Unimplemented CSI ?h parameter: " + params[i]); | |
943 } | |
944 } | |
945 } | |
946 else if (c == 108) { | |
947 /* l - reset modes */ | |
948 if (prefix != '?') { | |
949 debug(1, "Unimplemented CSI sequence: " + comstr); | |
950 return; | |
951 } | |
952 for (var i = 0; i < params.length; i++) { | |
953 if (params[i] == 1) { | |
954 keyHexCodes.appCursor(false); | |
955 } | |
956 else if (params[i] == 1048) { | |
957 this.restoreCursor(); | |
958 } | |
959 else if (params[i] == 1049) { | |
960 this.toNormBuf(); | |
961 } | |
962 else { | |
963 debug(1, "Unimplemented CSI ?l parameter: " + params[i]); | |
964 } | |
965 } | |
966 } | |
967 else if (c == 109) { | |
968 /* m - character attributes */ | |
969 if (params.length == 0) | |
970 this.clearAttrs(); | |
971 for (var i = 0; i < params.length; i++) { | |
972 if (params[i] == null || params[i] == 0) | |
973 this.clearAttrs(); | |
974 else if (params[i] == 1) | |
975 this.c.bold = true; | |
976 else if (params[i] == 4) | |
977 this.c.uline = true; | |
978 else if (params[i] == 7) | |
979 this.c.inverse = true; | |
980 else if (params[i] == 22) | |
981 this.c.bold = false; | |
982 else if (params[i] == 24) | |
983 this.c.uline = false; | |
984 else if (params[i] == 27) | |
985 this.c.inverse = false; | |
986 else if (params[i] >= 30 && params[i] <= 37) | |
987 this.c.fg = params[i] - 30; | |
988 else if (params[i] == 39) | |
989 this.c.fg = null; | |
990 else if (params[i] >= 40 && params[i] <= 47) | |
991 this.c.bg = params[i] - 40; | |
992 else if (params[i] == 49) | |
993 this.c.bg = null; | |
994 else if (params[i] >= 90 && params[i] <= 97) | |
995 this.c.fg = params[i] - 82; | |
996 else if (params[i] >= 100 && params[i] <= 107) | |
997 this.c.bg = params[i] - 92; | |
998 else if (params[i] == 38 && params[i + 1] == 5) { | |
999 if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256) | |
1000 this.c.fg = params[i+2]; | |
1001 i += 2; | |
1002 } | |
1003 else if (params[i] == 48 && params[i + 1] == 5) { | |
1004 if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256) | |
1005 this.c.bg = params[i+2]; | |
1006 i += 2; | |
1007 } | |
1008 else | |
1009 debug(1, "Unimplemented CSI m parameter: " + params[i]); | |
1010 } | |
1011 } | |
1012 else if (c == 114) { | |
1013 /* r - set scrolling region */ | |
1014 var t = 0; | |
1015 var b = 23; | |
1016 if (params[0] && params[0] <= 23) | |
1017 t = params[0] - 1; | |
1018 if (params[1] && params[1] <= 24) | |
1019 b = params[1] - 1; | |
1020 if (b <= t) | |
1021 return; | |
1022 this.scrT = t; | |
1023 this.scrB = b; | |
1024 this.cmove(0, 0); | |
1025 } | |
1026 else { | |
1027 debug(1, "Unimplemented CSI sequence: " + comstr); | |
1028 } | |
1029 return; | |
1030 }, | |
1031 oscProcess: function () { | |
1032 var numstr = ""; | |
1033 var i; | |
1034 for (i = 1; i < this.comseq.length; i++) { | |
1035 if (this.comseq[i] >= 48 && this.comseq[i] <= 57) | |
1036 numstr += String.fromCharCode(this.comseq[i]); | |
1037 else | |
1038 break; | |
1039 } | |
1040 if (this.comseq[i] != 59) { | |
1041 debug(1, "Invalid OSC sequence"); | |
1042 return; | |
1043 } | |
1044 var codenum = Number(numstr); | |
1045 var msgstr = ""; | |
1046 i++; | |
1047 while (i < this.comseq.length) { | |
1048 msgstr += String.fromCharCode(this.comseq[i]); | |
1049 i++; | |
1050 } | |
1051 if (codenum == 0 || codenum == 2) { | |
1052 setTitle(msgstr); | |
1053 } | |
1054 else | |
1055 debug(1, "Unimplemented OSC command " + codenum + " " + msgstr); | |
1056 return; | |
1057 }, | |
1058 makeCell: function(c) { | |
1059 var tnode; | |
1060 if (c && c.charAt && c.charAt(0)) | |
1061 tnode = document.createTextNode(c.charAt(0)); | |
1062 else | |
1063 tnode = document.createTextNode(' '); | |
1064 var cell = document.createElement("span"); | |
1065 cell.className = "termcell"; | |
1066 /* cssColor will handle reverse-video and bold->bright */ | |
1067 cell.style.color = this.cssColor(true); | |
1068 cell.style.backgroundColor = this.cssColor(false); | |
1069 if (this.c.bold) | |
1070 cell.style.fontWeight = "bold"; | |
1071 if (this.c.uline) | |
1072 cell.style.textDecoration = "underline"; | |
1073 cell.appendChild(tnode); | |
1074 return cell; | |
1075 }, | |
1076 makeRow: function() { | |
1077 var blankrow = document.createElement("div"); | |
1078 blankrow.className = "termrow"; | |
1079 for (var i = 0; i < 80; i++) | |
1080 blankrow.appendChild(this.makeCell(' ')); | |
1081 return blankrow; | |
1082 } | |
1083 }; | |
1084 | |
1085 function setTitle(tstr) { | |
1086 var titlespan = document.getElementById("ttitle"); | |
1087 var tnode = document.createTextNode(tstr); | |
1088 if (titlespan.childNodes.length == 0) | |
1089 titlespan.appendChild(tnode); | |
1090 else | |
1091 titlespan.replaceChild(tnode, titlespan.childNodes[0]); | |
1092 return; | |
1093 } | |
1094 | |
1095 function dchunk(codes) { | |
1096 var dstr = "Chunk: "; | |
1097 for (var i = 0; i < codes.length; i++) { | |
1098 if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160)) | |
1099 dstr += "\\x" + codes[i].toString(16); | |
1100 else | |
1101 dstr += String.fromCharCode(codes[i]); | |
1102 } | |
1103 debug(1, dstr); | |
1104 return; | |
1105 } |