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) {
+      debug(1, "Unrecognized CSI sequence: " + comstr);
+      return;
+    }
+    var prefix = null;
+    if (matchCSI[1])
+      prefix = matchCSI[1];
+    var postfix = null;
+    if (matchCSI[3])
+      postfix = matchCSI[3];
+    var params = [];
+    if (matchCSI[2]) {
+      var numstrs = matchCSI[2].split(";");
+      for (var i = 0; i < numstrs.length; i++) {
+        if (numstrs[i])
+          params.push(Number(numstrs[i]));
+        else
+          params.push(null);
+      }
+    }
+    /* Useful if expecting a single parameter which is a count. */
+    var count = 1;
+    if (params[0])
+      count = params[0];
+    /* The final character determines the action. */
+    if (c == 64) {
+      /* @ - insert spaces at cursor */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI @ sequence: " + comstr);
+        return;
+      }
+      /* The cursor stays still, but characters move out from under it. */
+      this.flipCursor();
+      var rowdiv = this.screen.childNodes[this.c.y];
+      var newspace;
+      while (count > 0) {
+        newspace = this.makeCell(' ');
+        rowdiv.insertBefore(newspace, rowdiv.childNodes[this.c.x]);
+        rowdiv.removeChild(rowdiv.lastChild);
+        count--;
+      }
+      /* Finally, put the cursor back. */
+      this.flipCursor();
+    }
+    else if (c >= 65 && c <= 71) {
+      /* A - up, B - down, C - forward, D - backward */
+      /* E - next line, F - previous line, G - to column */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI sequence: " + comstr);
+        return;
+      }
+      /* These may be out of range, but cmove will take care of that. */
+      if (c == 65)
+        this.cmove(this.c.y - count, null);
+      else if (c == 66)
+        this.cmove(this.c.y + count, null);
+      else if (c == 67)
+        this.cmove(null, this.c.x + count);
+      else if (c == 68)
+        this.cmove(null, this.c.x - count);
+      else if (c == 69)
+        this.cmove(this.c.y + count, 0);
+      else if (c == 70)
+        this.cmove(this.c.y - count, 0);
+      else if (c == 71)
+        this.cmove(null, count - 1);
+    }
+    else if (c == 72) {
+      /* H - move */
+      var x = 0;
+      var y = 0;
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI H sequence: " + comstr);
+        return;
+      }
+      if (params[0])
+        y = params[0] - 1;
+      if (params[1])
+        x = params[1] - 1;
+      if (y > 23)
+        y = 23;
+      if (x > 79)
+        x = 79;
+      debug(0, "Moving to row " + y + ", col " + x);
+      this.cmove(y, x);
+    }
+    else if (c == 73) {
+      /* I - move forward by tabs */
+      var x = this.c.x;
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI I sequence: " + comstr);
+        return;
+      }
+      while (count > 0) {
+        x = 8 * (Math.floor(x / 8) + 1);
+        if (x > 79) {
+          x = 79;
+          break;
+        }
+        count--;
+      }
+      this.cmove(null, x);
+    }
+    else if (c == 74) {
+      /* J - erase display */
+      var start;
+      var end;
+      var cols;
+      if (prefix == '?')
+        debug(1, "Warning: CSI ?J not implemented");
+      else if (prefix || postfix) {
+        debug(1, "Invalid CSI J sequence: " + comstr);
+        return;
+      }
+      if (!params[0]) {
+        /* Either 0 or not given */
+        start = this.c.y + 1;
+        end = 23;
+        cols = 1;
+      }
+      else if (params[0] == 1) {
+        start = 0;
+        end = this.c.y - 1;
+        cols = -1;
+      }
+      else if (params[0] == 2) {
+        start = 0;
+        end = 23;
+        cols = 0;
+      }
+      else {
+        debug(1, "Unimplemented parameter in CSI J sequence: " + comstr);
+        return;
+      }
+      for (var nrow = start; nrow <= end; nrow++) {
+        this.screen.replaceChild(this.makeRow(), this.screen.childNodes[nrow]);
+      }
+      if (cols != 0) {
+        /* Otherwise, the whole screen was erased and the active row doesn't
+         * need special treatment. */
+        var cursrow = this.screen.childNodes[this.c.y];
+        for (var ncol = this.c.x; ncol >= 0 && ncol < 80; ncol += cols) {
+          cursrow.replaceChild(this.makeCell(' '), cursrow.childNodes[ncol]);
+        }
+      }
+      this.offedge = false;
+      /* Always flip after replacing the active position. */
+      this.flipCursor();
+    }
+    else if (c == 75) {
+      /* K - erase line */
+      /* The ? is for an erase method that respects an "uneraseable" attribute,
+       * which isn't implemented, so the methods are equivalent for now. */
+      var start;
+      var end;
+      if (prefix == '?')
+        debug(1, "Warning: CSI ?K not implemented");
+      else if (prefix || postfix) {
+        debug(1, "Invalid CSI K sequence: " + comstr);
+        return;
+      }
+      /* 0 (default): right, 1: left, 2: all.  Include cursor position. */
+      if (params[0] == 1) {
+        start = 0;
+        end = this.c.x;
+      }
+      else if (params[0] == 2) {
+        start = 0;
+        end = 79;
+      }
+      else {
+        start = this.c.x;
+        end = 79;
+      }
+      var rowdiv = this.screen.childNodes[this.c.y];
+      for (var i = start; i <= end; i++) {
+        rowdiv.replaceChild(this.makeCell(' '), rowdiv.childNodes[i]);
+      }
+      /* Deleting stuff tends to clear this */
+      this.offedge = false;
+      /* The active position is always cleared, so the cursor must be made 
+       * visible again. */
+      this.flipCursor();
+    }
+    else if (c == 76 || c == 77) {
+      /* L - insert lines at the current position.
+       * M - delete current lines */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI sequence: " + comstr);
+        return;
+      }
+      /* CSI LM have no effect outside of the scrolling region */
+      if (this.c.y < this.scrT || this.c.y > this.scrB)
+        return;
+      this.flipCursor();
+      while (count > 0) {
+        var blankrow = this.makeRow();
+        if (c == 76) {
+          this.historize(this.scrB);
+          this.screen.insertBefore(blankrow, this.screen.childNodes[this.c.y]);
+        }
+        else {
+          if (this.scrB == 23)
+            this.screen.appendChild(blankrow);
+          else
+            this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB
+                    + 1]);
+          this.historize(this.c.y);
+        }
+        count--;
+      }
+      /* It doesn't seem necessary to reset this, but xterm does it. */
+      this.offedge = false;
+      this.flipCursor();
+    }
+    else if (c == 80) {
+      /* P - delete at active position, causing cells on the right to shift. */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI P sequence: " + comstr);
+        return;
+      }
+      var cursrow = this.screen.childNodes[this.c.y];
+      while (count > 0) {
+        cursrow.removeChild(cursrow.childNodes[this.c.x]);
+        cursrow.appendChild(this.makeCell(' '));
+        count--;
+      }
+      this.offedge = false;
+      this.flipCursor();
+    }
+    else if (c == 83 || c == 84) {
+      /* S - scroll up, T - scroll down */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI sequence: " + comstr);
+        return;
+      }
+      if (c == 83)
+        this.scroll(count);
+      else
+        this.scroll(-count);
+    }
+    else if (c == 88) {
+      /* X - erase characters */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI sequence: " + comstr);
+        return;
+      }
+      var row = this.screen.childNodes[this.c.y];
+      for (var i = 0; i < count && this.c.x + i < 80; i++) {
+        row.replaceChild(this.makeCell(' '), row.childNodes[this.c.x + i]);
+      }
+      this.flipCursor();
+    }
+    else if (c == 90) {
+      /* Z - tab backwards */
+      var x = this.c.x;
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI Z sequence: " + comstr);
+        return;
+      }
+      while (count > 0) {
+        x = 8 * (Math.ceil(x / 8) - 1);
+        if (x < 0) {
+          x = 0;
+          break;
+        }
+        count--;
+      }
+      this.cmove(null, x);
+    }
+    else if (c == 96) {
+      /* ` - go to col */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI ` sequence: " + comstr);
+        return;
+      }
+      this.cmove(null, count - 1);
+    }
+    else if (c == 100) {
+      /* d - go to row */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI d sequence: " + comstr);
+        return;
+      }
+      this.cmove(count - 1, null);
+    }
+    else if (c == 102) {
+      /* f - move */
+      var x = 0;
+      var y = 0;
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI f sequence: " + comstr);
+        return;
+      }
+      if (params[0])
+        y = params[0] - 1;
+      if (params[1])
+        x = params[1] - 1;
+      this.cmove(y, x);
+    }
+    else if (c == 104) {
+      /* h - set modes */
+      if (prefix != '?') {
+        debug(1, "Unimplemented CSI sequence: " + comstr);
+        return;
+      }
+      for (var i = 0; i < params.length; i++) {
+        if (params[i] == 1) {
+          keyHexCodes.appCursor(true);
+        }
+        else if (params[i] == 1048) {
+          this.saveCursor();
+        }
+        else if (params[i] == 1049) {
+          this.toAltBuf();
+        }
+        else {
+          debug(1, "Unimplemented CSI ?h parameter: " + params[i]);
+        }
+      }
+    }
+    else if (c == 108) {
+      /* l - reset modes */
+      if (prefix != '?') {
+        debug(1, "Unimplemented CSI sequence: " + comstr);
+        return;
+      }
+      for (var i = 0; i < params.length; i++) {
+        if (params[i] == 1) {
+          keyHexCodes.appCursor(false);
+        }
+        else if (params[i] == 1048) {
+          this.restoreCursor();
+        }
+        else if (params[i] == 1049) {
+          this.toNormBuf();
+        }
+        else {
+          debug(1, "Unimplemented CSI ?l parameter: " + params[i]);
+        }
+      }
+    }
+    else if (c == 109) {
+      /* m - character attributes */
+      if (params.length == 0)
+        this.clearAttrs();
+      for (var i = 0; i < params.length; i++) {
+        if (params[i] == null || params[i] == 0)
+          this.clearAttrs();
+        else if (params[i] == 1)
+          this.c.bold = true;
+        else if (params[i] == 4)
+          this.c.uline = true;
+        else if (params[i] == 7)
+          this.c.inverse = true;
+        else if (params[i] == 22)
+          this.c.bold = false;
+        else if (params[i] == 24)
+          this.c.uline = false;
+        else if (params[i] == 27)
+          this.c.inverse = false;
+        else if (params[i] >= 30 && params[i] <= 37)
+          this.c.fg = params[i] - 30;
+        else if (params[i] == 39)
+          this.c.fg = null;
+        else if (params[i] >= 40 && params[i] <= 47)
+          this.c.bg = params[i] - 40;
+        else if (params[i] == 49)
+          this.c.bg = null;
+        else if (params[i] >= 90 && params[i] <= 97)
+          this.c.fg = params[i] - 82;
+        else if (params[i] >= 100 && params[i] <= 107)
+          this.c.bg = params[i] - 92;
+        else if (params[i] == 38 && params[i + 1] == 5) {
+          if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256)
+            this.c.fg = params[i+2];
+          i += 2;
+        }
+        else if (params[i] == 48 && params[i + 1] == 5) {
+          if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256)
+            this.c.bg = params[i+2];
+          i += 2;
+        }
+        else
+          debug(1, "Unimplemented CSI m parameter: " + params[i]);
+      }
+    }
+    else if (c == 114) {
+      /* r - set scrolling region */
+      var t = 0;
+      var b = 23;
+      if (params[0] && params[0] <= 23)
+        t = params[0] - 1;
+      if (params[1] && params[1] <= 24)
+        b = params[1] - 1;
+      if (b <= t)
+        return;
+      this.scrT = t;
+      this.scrB = b;
+      this.cmove(0, 0);
+    }
+    else {
+      debug(1, "Unimplemented CSI sequence: " + comstr);
+    }
+    return;
+  },
+  oscProcess: function () {
+    var numstr = "";
+    var i;
+    for (i = 1; i < this.comseq.length; i++) {
+      if (this.comseq[i] >= 48 && this.comseq[i] <= 57)
+        numstr += String.fromCharCode(this.comseq[i]);
+      else
+        break;
+    }
+    if (this.comseq[i] != 59) {
+      debug(1, "Invalid OSC sequence");
+      return;
+    }
+    var codenum = Number(numstr);
+    var msgstr = "";
+    i++;
+    while (i < this.comseq.length) {
+      msgstr += String.fromCharCode(this.comseq[i]);
+      i++;
+    }
+    if (codenum == 0 || codenum == 2) {
+      setTitle(msgstr);
+    }
+    else
+      debug(1, "Unimplemented OSC command " + codenum + " " + msgstr);
+    return;
+  },
+  makeCell: function(c) {
+    var tnode;
+    if (c && c.charAt && c.charAt(0))
+      tnode = document.createTextNode(c.charAt(0));
+    else
+      tnode = document.createTextNode(' ');
+    var cell = document.createElement("span");
+    cell.className = "termcell";
+    /* cssColor will handle reverse-video and bold->bright */
+    cell.style.color = this.cssColor(true);
+    cell.style.backgroundColor = this.cssColor(false);
+    if (this.c.bold)
+      cell.style.fontWeight = "bold";
+    if (this.c.uline)
+      cell.style.textDecoration = "underline";
+    cell.appendChild(tnode);
+    return cell;
+  },
+  makeRow: function() {
+    var blankrow = document.createElement("div");
+    blankrow.className = "termrow";
+    for (var i = 0; i < 80; i++)
+      blankrow.appendChild(this.makeCell(' '));
+    return blankrow;
+  }
+};
+
+function setTitle(tstr) {
+  var titlespan = document.getElementById("ttitle");
+  var tnode = document.createTextNode(tstr);
+  if (titlespan.childNodes.length == 0)
+    titlespan.appendChild(tnode);
+  else
+    titlespan.replaceChild(tnode, titlespan.childNodes[0]);
+  return;
+}
+
+function dchunk(codes) {
+  var dstr = "Chunk: ";
+  for (var i = 0; i < codes.length; i++) {
+    if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160))
+      dstr += "\\x" + codes[i].toString(16);
+    else
+      dstr += String.fromCharCode(codes[i]);
+  }
+  debug(1, dstr);
+  return;
+}