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); | |
