view termemu.js @ 17:d3e3d6b4016b

rlgwebd: switch to dgamelaunch's SQLite database. The quickrypt utility is replaced with sqlickrypt, which takes a username and password pair and checks them against the SQLite password database used by dgamelaunch. This will be more extensible to using rlgwebd to register, change passwords, etc.
author John "Elwin" Edwards <elwin@sdf.org>
date Sun, 20 May 2012 15:52:07 -0700
parents 826a7ced69f8
children de01aafd4dd6
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 = {
  sessid: null, // Session key assigned by the server
  alive: false,
  /* 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");