diff termemu.js @ 0:bd412f63ce0d

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