Mercurial > hg > rlgwebd
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/termemu.js Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,1105 @@ +/* termemu.js: a mostly xterm-compatible terminal emulator for a webpage */ +/* SELF-HOSTING 2011-09-23 */ + +// How detailed the debugging should be. +var debugSuppress = 1; +// Some char values. +var csiPre = [63, 62, 33]; +var csiPost = [36, 34, 39, 32]; +function csiFinal(code) { + /* @A-Z */ + if (code >= 64 && code <= 90) + return true; + /* `a-z{| */ + if (code >= 96 && code <= 124) + return true; + return false; +} +var esc7ctl = [68, 69, 72, 77, 78, 79, 80, 86, 87, 88, 90, 91, 92, 93, 94, 95]; +var escSingle = [55, 56, 61, 62, 70, 99, 108, 109, 110, 111, 124, 125, 126]; +var escDouble = [32, 35, 37, 40, 41, 42, 43, 45, 46, 47]; + +var decChars = {96: 0x2666, 97: 0x2592, 102: 0xB0, 103: 0xB1, + 106: 0x2518, 107: 0x2510, 108: 0x250C, 109: 0x2514, + 110: 0x253C, 111: 0x23BA, 112: 0x23BB, 113: 0x2500, + 114: 0x23BC, 115: 0x23BD, 116: 0x251C, 117: 0x2524, + 118: 0x2534, 119: 0x252C, 120: 0x2502, 121: 0x2264, + 122: 0x2265, 123: 0x03C0, 124: 0x2260, 125: 0xA3, 126: 0xB7}; + +/* Not everything that should be saved by DECSC has been implemented yet. */ +function Cursor(src) { + if (src) { + this.x = src.x; + this.y = src.y; + this.bold = src.bold; + this.inverse = src.inverse; + this.uline = src.uline; + this.fg = src.fg; + this.bg = src.bg; + this.cset = src.cset; + } + else { + this.x = 0; + this.y = 0; + this.bold = false; + this.inverse = false; + this.uline = false; + this.fg = null; + this.bg = null; + this.cset = "B"; + } + return; +} + +// An object representing the terminal emulator. +var termemu = { + sessid: null, // Session key assigned by the server + /* Some elements of the page. */ + inwrap: null, // A non-table div wrapping the screen + view: null, // The div holding the terminal screen + screen: null, // The div representing the active screen area + normbuf: null, // The normal screen buffer + altbuf: null, // The alternate screen buffer + histbuf: null, // The screen history buffer + fgColor: "#b2b2b2", // Default color for text + bgColor: "black", // Default background color + c: null, // Contains cursor position and text attributes + offedge: false, // Going off the edge doesn't mean adding a new line + clearAttrs: function () { + /* Make sure to reset ALL attribute properties and NOTHING else. */ + this.c.bold = false; + this.c.inverse = false; + this.c.uline = false; + this.c.fg = null; + this.c.bg = null; + }, + saved: null, // saved cursor + normc: null, // Stores the normal screen buffer cursor when using altbuf + ansicolors: ["#000000", "#b21818", "#18b218", "#b26818", "#1818b2", + "#b218b2", "#18b2b2", "#b2b2b2"], + brightcolors: ["#686868", "#ff5454", "#54ff54", "#ffff54", "#5454ff", + "#ff54ff", "#54ffff", "#ffffff"], + cssColor: function (fg) { + /* returns a CSS color specification for the text or background */ + var n; + var fallback; + var cube6 = ["00", "5f", "87", "af", "d7", "ff"]; + if (this.c.inverse) + fg = !fg; + if (fg) { + n = this.c.fg; + fallback = this.fgColor; + if (n == null) + return fallback; + } + else { + n = this.c.bg; + fallback = this.bgColor; + if (n == null) + return fallback; + } + if (n < 0) + return fallback; + else if (n < 8) { + if (this.c.bold && fg) + return this.brightcolors[n]; + else + return this.ansicolors[n]; + } + else if (n < 16) + return this.brightcolors[n - 8]; + else if (n < 232) { + var r = cube6[Math.floor((n - 16) / 36)]; + var g = cube6[Math.floor((n - 16) / 6) % 6]; + var b = cube6[(n - 16) % 6]; + return "#" + r + g + b; + } + else if (n < 256) { + var colstr = ((n - 232) * 10 + 8).toString(16); + if (colstr.length < 2) + colstr = "0" + colstr; + return "#" + colstr + colstr + colstr; + } + else + return fallback; + }, + scrT: 0, // top and bottom of scrolling region + scrB: 23, + // These keyboard-related things don't really belong here. + shift: false, + shiftp: function () { + return this.shift; + }, + toggleshift: function () { + this.shift = !this.shift; + }, + ctrl: false, + ctrlp: function () { + return this.ctrl; + }, + togglectrl: function () { + this.ctrl = !this.ctrl; + }, + init: function (divID) { + /* Makes a div full of character cells. */ + if (this.screen != null) + return; + var owrap = document.getElementById(divID); + if (!owrap) + return; + while (owrap.firstChild != null) + owrap.removeChild(owrap.firstChild); + this.c = new Cursor(null); + /* Create the contents of the terminal div */ + this.inwrap = document.createElement("div"); + this.inwrap.id = "inwrap"; + owrap.appendChild(this.inwrap); + var termdiv = document.createElement("div"); + termdiv.id = "term"; + termdiv.style.fontSize = "12px"; + this.inwrap.appendChild(termdiv); + /* Set up the screen buffers */ + this.histbuf = document.createElement("div"); + this.histbuf.id = "histbuf"; + termdiv.appendChild(this.histbuf); + this.normbuf = document.createElement("div"); + this.normbuf.id = "normbuf"; + termdiv.appendChild(this.normbuf); + for (var row = 0; row < 24; row++) { + this.normbuf.appendChild(this.makeRow()); + } + this.altbuf = document.createElement("div"); + this.altbuf.id = "altbuf"; + termdiv.appendChild(this.altbuf); + this.altbuf.style.display = "none"; + /* altbuf will be filled when it is used. */ + /* Attach them. */ + this.view = termdiv; + this.screen = this.normbuf; + this.resize(); + this.cmove(0, 0); + }, + valign: function () { + if (this.screen == this.normbuf) + this.inwrap.scrollTop = this.histbuf.clientHeight; + }, + resize: function () { + var owrap = document.getElementById("termwrap"); + /* Set the height up properly. */ + this.inwrap.style.height = this.screen.scrollHeight.toString() + "px"; + this.valign(); + // Figure out how wide the vertical scrollbar is. + var dwid = this.inwrap.offsetWidth - this.inwrap.clientWidth; + // And resize accordingly. + this.inwrap.style.width = (this.view.scrollWidth + dwid).toString() + "px"; + owrap.style.width = this.inwrap.offsetWidth.toString() + "px"; + return; + }, + comseq: [], // Part of an impending control sequence + flipCursor: function () { + /* Swaps the text and background colors of the active location. */ + /* This will change when other cursor styles are supported. */ + if (this.c.x != null && this.c.y != null) { + var oldcell = this.screen.childNodes[this.c.y].childNodes[this.c.x]; + var tempswap = oldcell.style.color; + oldcell.style.color = oldcell.style.backgroundColor; + oldcell.style.backgroundColor = tempswap; + } + return; + }, + saveCursor: function () { + this.saved = new Cursor(this.c); + return; + }, + restoreCursor: function () { + if (!this.saved) { + this.cmove(0, 0); + this.c = new Cursor(null); + } + else { + this.cmove(this.saved.y, this.saved.x); + this.c = new Cursor(this.saved); + } + return; + }, + toAltBuf: function () { + if (this.screen == this.altbuf) + return; + while (this.altbuf.firstChild != null) + this.altbuf.removeChild(this.altbuf.firstChild); + for (var i = 0; i < 24; i++) { + this.altbuf.appendChild(this.makeRow()); + } + this.normc = new Cursor(this.c); + this.altbuf.style.display = "table-row-group"; + this.normbuf.style.display = "none"; + this.histbuf.style.display = "none"; + this.screen = this.altbuf; + debug(0, "Altbuf with charset " + this.c.cset); + return; + }, + toNormBuf: function () { + if (this.screen == this.normbuf) + return; + this.altbuf.style.display = "none"; + this.normbuf.style.display = "table-row-group"; + this.histbuf.style.display = "table-row-group"; + this.screen = this.normbuf; + this.valign(); + /* The cursor isn't actually at this position in normbuf, but cmove will + * flip it anyway. Flip it again to compensate. */ + this.flipCursor(); + this.cmove(this.normc.y, this.normc.x); + this.c = new Cursor(this.normc); + }, + cmove: function (y, x) { + /* Move the cursor. NOTE coords are [row, col] as in curses. */ + /* If x or y is null, that coordinate is not altered. */ + /* Sanity checks and initializations. */ + if (x == null) { + if (this.c.x != null) + x = this.c.x; + else + return; + } + else { + this.offedge = false; + if (x < 0) + x = 0; + else if (x > 79) + x = 79; + } + if (y == null) { + if (this.c.y != null) + y = this.c.y; + else + return; + } + else if (y < 0) + y = 0; + else if (y > 23) + y = 23; + /* Un-reverse video the current location. */ + this.flipCursor(); + this.c.x = x; + this.c.y = y; + /* Reverse-video the new location. */ + this.flipCursor(); + return; + }, + historize: function (n) { + if (n < 0 || n >= this.screen.childNodes.length) + return; + var oldrow = this.screen.childNodes[n]; + if (this.screen != this.altbuf && this.scrT == 0) { + this.histbuf.appendChild(oldrow); + } + else { + this.screen.removeChild(oldrow); + } + /* These may not be the correct heights... */ + this.inwrap.style.height = this.screen.clientHeight.toString() + "px"; + this.valign(); + }, + scroll: function (lines) { + if (lines == 0) + return; + var count; + if (lines > 0) + count = lines; + else + count = -lines; + this.flipCursor(); + while (count > 0) { + var blankrow = this.makeRow(); + /* Careful with the order */ + if (lines > 0) { + if (this.scrB == 23) + this.screen.appendChild(blankrow); + else + this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB + + 1]); + this.historize(this.scrT); + } + else { + /* Historize here? */ + this.screen.removeChild(this.screen.childNodes[this.scrB]); + this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrT]); + } + count--; + } + this.valign(); // needed? + this.flipCursor(); + return; + }, + newline: function (doReturn) { + if (this.c.y == this.scrB) + this.scroll(1) + else if (this.c.y < 23) + this.cmove(this.c.y + 1, null); + /* If the cursor is at the bottom but outside the scrolling region, + * nothing can be done. */ + if (doReturn) { + this.cmove(null, 0); + } + }, + antinewline: function () { + if (this.c.y == this.scrT) + this.scroll(-1); + else if (this.c.y > 0) + this.cmove(this.c.y - 1, null); + }, + advance: function () { + if (this.c.x < 79) + this.cmove(null, this.c.x + 1); + else { + this.offedge = true; + } + }, + placechar: function (str) { + if (this.offedge) { + this.newline(true); + } + var nextch = str.charAt(0); + var newcell = this.makeCell(nextch); + var rowdiv = this.screen.childNodes[this.c.y]; + rowdiv.replaceChild(newcell, rowdiv.childNodes[this.c.x]); + this.flipCursor(); // The replace removed the cursor. + /* Update the cursor. */ + this.advance(); + }, + reset: function () { + /* Reset ALL state, hopefully in the right order. */ + /* TODO test this and compare it with xterm */ + this.toNormBuf(); + this.clearAttrs(); + this.c.cset = 'B'; + this.cmove(0, 0); + this.saved = null; + this.normc = null; + this.scrT = 0; + this.scrB = 23; + while (this.histbuf.firstChild != null) { + this.histbuf.removeChild(this.histbuf.firstChild); + } + for (var i = 0; i < 24; i++) { + this.screen.replaceChild(this.makeRow(), this.screen.childNodes[i]); + } + this.flipCursor(); // make it appear in the new row + return; + }, + write: function (codes) { + //dchunk(codes); + for (var i = 0; i < codes.length; i++) { + /* First see if there's an incomplete command sequence waiting. */ + if (this.comseq.length > 0) { + if (this.comseq.length == 1 && this.comseq[0] == 27) { + /* Just ESC */ + if (codes[i] == 55) { + /* ESC 7 : save cursor */ + this.saveCursor(); + this.comseq = []; + } + else if (codes[i] == 56) { + /* ESC 8 : restore cursor */ + this.restoreCursor(); + this.comseq = []; + } + else if (codes[i] == 61) { + /* ESC = : application keypad */ + keyHexCodes.appKeypad(true); + this.comseq = []; + } + else if (codes[i] == 62) { + /* ESC > : normal keypad */ + keyHexCodes.appKeypad(false); + this.comseq = []; + } + else if (codes[i] == 68) { + /* ESC D = IND */ + this.newline(false); + this.comseq = []; + } + else if (codes[i] == 69) { + /* ESC E = NEL */ + this.newline(true); + this.comseq = []; + } + else if (codes[i] == 77) { + /* ESC M = RI */ + this.antinewline(); + this.comseq = []; + } + else if (codes[i] == 91) { + /* ESC [ = CSI */ + this.comseq[0] = 155; + } + else if (codes[i] == 93) { + /* ESC [ = OSC */ + this.comseq[0] = 157; + } + else if (codes[i] == 99) { + /* ESC c = reset */ + this.reset(); + this.comseq = []; + } + else if (escSingle.indexOf(codes[i]) >= 0) { + /* An unimplemented two-char sequence. */ + debug(1, "Unimplemented sequence ESC " + codes[i].toString(16)); + this.comseq = []; + } + else if (escDouble.indexOf(codes[i]) >= 0) { + /* A three-char sequence. */ + this.comseq.push(codes[i]); + } + else { + /* Nothing else is implemented yet. */ + debug(1, "Unrecognized sequence ESC " + codes[i].toString(16)); + this.comseq = []; + } + } + else if (this.comseq.length == 2 && this.comseq[0] == 27) { + /* An ESC C N sequence. Not implemented. Doesn't check validity + * of N. */ + if (this.comseq[1] == 40) { + if (codes[i] == 48) { + this.c.cset = "0"; + debug(0, "Switching to DEC graphics."); + } + else if (codes[i] == 66) { + this.c.cset = "B"; + debug(0, "Switching to ASCII."); + } + else { + debug(1, "Unimplemented character set: " + + String.fromCharCode(codes[i])); + } + debug(0, "cset is now: " + this.c.cset); + } + else + debug(1, "Unknown sequence ESC " + + String.fromCharCode(this.comseq[1]) + " 0x" + + codes[i].toString(16)); + this.comseq = []; + } + else if (this.comseq[0] == 157) { + /* Commands beginning with OSC */ + /* Check for string terminator */ + if (codes[i] == 7 || codes[i] == 156 || (codes[i] == 92 && + this.comseq[this.comseq.length - 1] == 27)) { + if (codes[i] == 92 && this.comseq[this.comseq.length - 1] == 27) + this.comseq.pop(); + debug(0, "Got " + (this.comseq.length - 1) + "-byte OSC sequence"); + this.oscProcess(); + this.comseq = []; + } + else + this.comseq.push(codes[i]); + } + else if (this.comseq[0] == 155) { + /* Commands starting with CSI */ + // End at first char that's not numeric ; ? > ! $ " space ' + // i.e. letter @ ` lbrace | + // ?>! must come directly after CSI + // $"'space must come directly before terminator + // FIXME put this checking code into csiProcess + if (csiPre.indexOf(codes[i]) >= 0) { + if (this.comseq.length > 1) { + /* Chars in csiPre can only occur right after the CSI */ + debug(1, "Invalid CSI sequence: misplaced prefix"); + this.comseq = []; + } + else + this.comseq.push(codes[i]); + } + else if (csiPost.indexOf(this.comseq[this.comseq.length - 1]) >= 0 && + !csiFinal(codes[i])) { + /* Chars is csiPost must come right before the final char */ + debug(1, "Invalid CSI sequence: misplaced postfix"); + this.comseq = []; + } + else if ((codes[i] >= 48 && codes[i] <= 57) || codes[i] == 59 || + csiPost.indexOf(codes[i]) >= 0) { + /* Numbers and ; can go anywhere */ + this.comseq.push(codes[i]); + } + else if (csiFinal(codes[i])) { + this.comseq.push(codes[i]); + this.csiProcess(); + this.comseq = []; + } + else { + debug(1, "Invalid CSI sequence: unknown code " + codes[i].toString(16)); + this.comseq = []; + } + } + else { + debug(1, "Unknown sequence with " + this.comseq[0].toString(16)); + this.comseq = []; + } + continue; + } + /* Treat it as a single character. */ + if (codes[i] == 5) { + sendback("06"); + } + else if (codes[i] == 7) { + /* bell */ + bell(true); + } + else if (codes[i] == 8) { + /* backspace */ + if (this.offedge) + this.offedge = false; + else if (this.c.x > 0) + this.cmove(null, this.c.x - 1); + } + else if (codes[i] == 9) { + /* tab */ + var xnew; + if (this.c.x < 79) { + xnew = 8 * (Math.floor(this.c.x / 8) + 1); + if (xnew > 79) + xnew = 79; + this.cmove(null, xnew); + } + else { + this.offedge = true; + } + } + else if (codes[i] >= 10 && codes[i] <= 12) { + /* newline, vertical tab, form feed */ + if (this.offedge) + this.newline(true); + else + this.newline(false); + } + else if (codes[i] == 13) { + /* carriage return \r */ + this.cmove(null, 0); + } + else if (codes[i] == 14) { + /* shift out */ + // Currently assuming that G1 is DEC Special & Line Drawing + this.c.cset = "0"; + debug(0, "Using DEC graphics charset."); + } + else if (codes[i] == 15) { + /* shift in */ + // Currently assuming that G0 is ASCII + this.c.cset = "B"; + debug(0, "Using ASCII charset."); + } + else if (codes[i] == 27) { + /* escape */ + this.comseq.push(codes[i]); + } + else if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160)) { + /* Some kind of control character. */ + debug(1, "Unprintable character 0x" + codes[i].toString(16)); + } + else { + /* If it's ASCII, it's printable; take a risk on anything higher */ + if ((this.c.cset == "0") && (codes[i] in decChars)) { + // DEC special character set + this.placechar(String.fromCharCode(decChars[codes[i]])); + } + else { + this.placechar(String.fromCharCode(codes[i])); + } + } + } + return; + }, + csiProcess: function () { + /* Processes the CSI sequence in this.comseq */ + var c = this.comseq[this.comseq.length - 1]; + if (this.comseq[0] != 155 || !csiFinal(c)) + return; + var comstr = ""; + for (var i = 1; i < this.comseq.length; i++) + comstr += String.fromCharCode(this.comseq[i]); + debug(0, "CSI sequence: " + comstr); + var reCSI = /^([>?!])?([0-9;]*)([ "$'])?([A-Za-z@`{|])$/; + var matchCSI = comstr.match(reCSI); + if (!matchCSI) {