view termemu.js @ 138:144595e50376

termemu.js: handle some tmux-produced control sequences.
author John "Elwin" Edwards
date Thu, 18 Jul 2013 10:43:41 -0700
parents a4d3ecf188b7
children 6758d832c10c
line wrap: on
line source

/* 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 = {
  /* 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
  /* Attributes */
  w: 0, // Screen width
  h: 0, // Screen height
  fgColor: "#b2b2b2", // Default color for text
  bgColor: "black", // Default background color
  scrT: 0, // top and bottom of scrolling region
  scrB: 0, // init() will set this properly
  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;
  },
  // 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, h, w) {
    /* 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);
    /* Set up the sizes. */
    var tn;
    tn = Number(h);
    if (tn > 0 && tn < 256)
      this.h = tn;
    else
      this.h = 24;
    tn = Number(w);
    if (tn > 0 && tn < 256)
      this.w = tn;
    else
      this.w = 80;
    this.scrB = this.h - 1;
    /* 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 < this.h; 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.fixsize();
    this.cmove(0, 0);
  },
  resize: function (h, w) {
    if (this.screen == null)
      return;
    if (!(h > 0 && h < 256))
      h = this.h;
    if (!(w > 0 && w < 256))
      w = this.w;
    /* First give all the rows the right number of cells. */
    var allrows = Array().concat(this.histbuf.childNodes, 
                                 this.normbuf.childNodes, 
                                 this.altbuf.childNodes);
    for (var i = 0; i < allrows.length; i++) {
      var row = allrows[i];
      for (var j = Math.min(w, this.w); j < Math.max(w, this.w); j++) {
        if (w < this.w)
          row.removeChild(row.childNodes.lastChild);
        else
          row.appendChild(this.makeCell(' '));
      }
    }
    this.w = w;
    /* Now the rows. */
    /* Resizing altbuf isn't always necessary. */
    /* TODO resize and scrolling region interact in complicated ways that I 
     * don't want to reverse-engineer.  For the moment, just don't do that.
     */
    if (h > this.h) {
      for (var i = this.h; i < h; i++) {
        this.normbuf.appendChild(this.makeRow());
        this.altbuf.appendChild(this.makeRow());
      }
    }
    else if (h < this.h) {
      for (var i = h; i < this.h; i++) {
        this.histbuf.appendChild(this.normbuf.firstChild);
        if (this.altbuf.firstChild)
          this.altbuf.removeChild(this.altbuf.firstChild);
      }
    }
    /* Keep it on the bottom */
    if (this.scrB == this.h - 1)
      this.scrB = h - 1;
    else if (this.scrB >= h)
      this.scrB = h - 1;
    if (this.scrT >= h - 1)
      this.scrT = 0;
    this.h = h;
    this.fixsize();
    /* If the cursor is now offscreen, cmove()'s sanity checks will fix it. */
    this.cmove(null, null);
    debug(1, "Size is now " + this.w + "x" + this.h);
    return;
  },
  valign: function () {
    if (this.screen == this.normbuf)
      this.inwrap.scrollTop = this.histbuf.clientHeight;
  },
  fixsize: 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. */
    /* Check: the cursor might be offscreen if it was resized. */
    if (this.c.x != null && this.c.y != null && this.c.x >= 0 &&
        this.c.x < this.w && this.c.y >= 0 && this.c.y < this.h) {
      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 < this.h; 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 >= this.w)
        x = this.w - 1;
    }
    if (y == null) {
      if (this.c.y != null)
        y = this.c.y;
      else
        return;
    }
    else if (y < 0)
      y = 0;
    else if (y >= this.h)
      y = this.h - 1;
    /* 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 == this.h - 1)
          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 < this.h - 1)
      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 < this.w - 1)
      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 = this.h - 1;
    while (this.histbuf.firstChild != null) {
      this.histbuf.removeChild(this.histbuf.firstChild);
    }
    for (var i = 0; i < this.h; 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 < this.w - 1) {
          xnew = 8 * (Math.floor(this.c.x / 8) + 1);
          if (xnew >= this.w)
            xnew = this.w - 1;
          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 >= this.h)
        y = this.h - 1;
      if (x >= this.w)
        x = this.w - 1;
      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 >= this.w) {
          x = this.w - 1;
          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 = this.h - 1;
        cols = 1;
      }
      else if (params[0] == 1) {
        start = 0;
        end = this.c.y - 1;
        cols = -1;
      }
      else if (params[0] == 2) {
        start = 0;
        end = this.h - 1;
        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 < this.w; 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 = this.w - 1;
      }
      else {
        start = this.c.x;
        end = this.w - 1;
      }
      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 == this.h - 1)
            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 < this.w; 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] == 7) {
          /* Wraparound ON is the only mode implemented. */
          debug(0, "Wraparound mode");
        }
        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 != '?') {
        if (prefix === null && params[0] == 4) {
          /* Insert mode is not implemented. */
          debug(0, "Replace mode");
        }
        else {
          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 (prefix !== null) {
        debug(1, "Unimplemented CSI sequence: " + comstr);
        return;
      }
      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 = this.h - 1;
      if (params[0] && params[0] <= this.h - 1)
        t = params[0] - 1;
      if (params[1] && params[1] <= this.h)
        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 (i < this.comseq.length && this.comseq[i] != 59) {
      debug(1, "Invalid OSC sequence: " + this.comseq.toString());
      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 < this.w; i++)
      blankrow.appendChild(this.makeCell(' '));
    return blankrow;
  }
};

function dchunk(codes) {
  var dstr = "Chunk: ";
  for (var i = 0; i < codes.length; i++) {
    if (codes[i] == 27)
      dstr += "\\e";
    else if (codes[i] == 92)
      dstr += "\\\\";
    else 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;
}