view termemu.js @ 170:50e4c9feeac2

RLGWebD: fix simultaneous player bug. Multiple games can now run at the same time, and data will be sent to the proper place. The interaction of multiple players with watchers has not yet been tested.
author John "Elwin" Edwards
date Fri, 09 Jan 2015 13:06:41 -0500
parents 1d3cfe10974a
children d60063a674e1
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;
    this.visible = src.visible;
  }
  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";
    this.visible = true;
  }
  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.visible)
      return;
    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 == 99) {
      /* c - query terminal attributes */
      if (prefix !== null) {
        debug(1, "Unimplemented CSI sequence: " + comstr);
        return;
      }
      /* "CSI ? 1 ; 2 c" - VT100 */
      sendback("1b5b3f313b3263");
    }
    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] == 25) {
          /* Show the cursor. */
          if (!this.c.visible) {
            this.c.visible = true;
            this.flipCursor();
          }
          debug(0, "Showing cursor");
        }
        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] == 12) {
          debug(0, "Stopping blinking cursor");
        }
        else if (params[i] == 25) {
          /* Hide the cursor. */
          if (this.c.visible) {
            this.flipCursor();
            this.c.visible = false;
          }
          debug(0, "Hiding cursor");
        }
        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 if (codenum >= 110 && codenum <= 118) {
      /* Reset colors that can't be changed yet anyway */
      debug(0, "Resetting dynamic color " + codenum);
    }
    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;
}