Logo  

Home - Old Man Programmer

Displaying webapps/vt100/vt100.js

var terminal = {
  FGMASK	: 15,
  BGMASK	: 240,
  BOLD		: 256,
  UNDERLINE	: 512,
  REVERSE	: 1024,
  BLINK		: 2048,

  colors : [
    "#000000", "#cd0000", "#00cd00", "#cdcd00", "#0000ee", "#cd00cd", "#00cdcd", "#e5e5e5",
    "#7f7f7f", "#ff0000", "#00ff00", "#ffff00", "#5c5cff", "#ff00ff", "#00ffff", "#ffffff"
  ],
  window : null, scrollback : null, tc : null,
  kbmode : false,

  sc : null,
  m : null, row : null,
  // m: { e : col, char : " ", fg : this.fg, bg: this.bg };

  rows : 0, cols : 0,
  homeRow: 0, homeCol : 0,
  tmargin: 0, bmargin: 0,
  cx : 0, cy : 0,
  cq : "", cqlen : 0,

  // Attributes:
  fg : 7, bg : 0,
  bold: false, underline: false, blink: false, reverse: false,
  
  enablescrollback : false,
  sbbuffer : [],
  sbtimer : null,

  mode : 0,
  // 0 = normal, 1 = escape encountered:
  //   2 = [ encountered, 3 = [? encountered,
  //   4 = ) encountered, 5 = ( encountered

  sbupdate : function() {
    for(var i = 0; i < terminal.sbbuffer.length; i++)
      terminal.scrollback.appendChild(terminal.sbbuffer[i]);
    terminal.sbbuffer = [];
    terminal.sbtimer = null;
    terminal.tc.scrollTop = terminal.tc.scrollHeight+30;
  },

  newrow : function(oldline) {
    var row = document.createElement("tr");
    var m = [];
    for(var c = 0; c < this.cols; c ++) {
      var col = document.createElement("td");
      col.innerHTML = "&nbsp;";
      if (oldline != undefined) {
	m[c] = { e : col, char : " ", fg : oldline[c].fg, bg: oldline[c].bg};
      } else {
	m[c] = { e : col, char : " ", fg : this.fg, bg: this.bg };
      }
      this.setColor(m[c], m[c].fg, m[c].bg);
      row.appendChild(col);
    }
    return { m: m, row: row };
  },
  scrollup : function() {
    if (this.tmargin == 0 && this.enablescrollback) {
      this.sbbuffer.push(this.row[0]);
      if (this.sbtimer != null) clearTimeout(this.sbtimer);
      this.sbtimer = setTimeout(this.sbupdate, 100);
    }
    this.window.removeChild(this.row[this.tmargin]);

    var nr = this.newrow();
    for(var r = this.tmargin; r < this.bmargin; r++) {
      this.m[r] = this.m[r+1];
      this.row[r] = this.row[r+1];
    }
    this.m[this.bmargin] = nr.m;
    this.row[this.bmargin] = nr.row;
    if (this.bmargin == this.rows-1) {
      this.window.appendChild(nr.row);
      this.tc.scrollTop = this.tc.scrollHeight+30;
    } else
      this.window.insertBefore(nr.row, this.row[this.bmargin+1]);
  },
  scrolldown : function() {
    var nr = this.newrow();
    var old = this.row[this.bmargin];

    for(var r = this.bmargin; r > this.tmargin; r--) {
      this.m[r] = this.m[r-1];
      this.row[r] = this.row[r-1];
    }
    this.m[this.tmargin] = nr.m;
    this.row[this.tmargin] = nr.row;
    this.window.insertBefore(nr.row, this.row[this.tmargin+1]);
    this.window.removeChild(old);
  },

  tab : function() {
    this.cx = Math.min(this.cols-1, (8*Math.floor((this.cx+8)/8)));
  },
  cr : function() {
    this.cx = 0;
  },
  down : function(lines, scroll) {
    do {
      if (this.cy == this.bmargin) {
	if (scroll) this.scrollup();
      } else this.cy++;
    } while (--lines > 0);
  },
  up : function(lines, scroll) {
    do {
      if (this.cy == this.tmargin) {
	if (scroll) this.scrolldown();
      } else this.cy--;
    } while (--lines > 0);
  },
  left : function(cols) {
    this.cx = Math.max(0, this.cx-(cols?cols:1));
  },
  right : function(cols) {
    this.cx = Math.min(this.cols-1, this.cx + (cols? cols : 1));
  },
  move : function(y, x) {
    this.cy = Math.max(Math.min(this.rows-1,y-1), 0);
    this.cx = Math.max(Math.min(this.cols-1,x-1), 0);
  },
  clrline : function(r, s, e) {
    var m = this.m[r];
    for(var c = s; c <= e; c++) {
      m[c].char = " ";
      m[c].e.innerHTML = "&nbsp;";
      this.setColor(m[c], this.fg, this.bg);
    }
  },
  eid : function(n) {
    switch(n) {
      case 0:	// erase cursor to eos
	this.clrline(this.cy, this.cx, this.cols-1);
	for(var r = this.cy+1; r < this.rows; r++)
	  this.clrline(r, 0, this.cols-1);
	break;
      case 1:	// start of screen to cursor
	for(var r = 0; r < this.cy; r++)
	  this.clrline(r, 0, this.cols-1);
	this.clrline(this.cy, 0, this.cx);
	break;
      case 2:	// clear screen
	for(var r = 0; r < this.rows; r++)
	  this.clrline(r, 0, this.cols-1);
    }
  },
  eil : function(n) {
    switch(n) {
      case 0:	// erase cursor to eol
	return this.clrline(this.cy, this.cx, this.cols-1);
      case 1:	// erase from bol to cursor
	return this.clrline(this.cy, 0, this.cx);
      case 2:	// erase entire line
	return this.clrline(this.cy, 0, this.cols-1);
    }
  },

  setMargins : function(t, b) {
    t = Math.min(Math.max(0, t-1), this.rows-2);
    b = Math.min(Math.max(0, b-1), this.rows-1);
    if (t == 0 && b == 0) b = this.rows-1;
    if (t >= b) return;
    this.tmargin = t;
    this.bmargin = b;
    this.move(this.homeRow, this.homeCol);
    return;
  },

  setAttrs : function(pn) {
    var v, l = Math.max(this.cqlen, 1);
    if (this.cqlen == 0) pn[0] = 0;

    while (l-- > 0) {
      switch(v = pn[0]) {
	case 0:
	  this.fg = 7; this.bg = 0;
	  this.bold = this.underline = this.blink = this.reverse = false;
	  break;
	case 1:
	  this.bold = true;
	  break;
	case 4:
	  this.underline = true;
	  break;
	case 5:
	  this.blink = true;
	  break;
	case 7:
	  this.reverse = true;
	  break;
	case 8:
	  this.fg = this.bg;
	  break;
	case 21:
	  this.bold = false;
	  break;
	case 24:
	  this.underline = false;
	  break;
	case 27:
	  this.reverse = false;
	  break;
	case 28:
	  this.fg = 0;
	  break;
	default:
	  if (v >= 30 && v <= 37) this.fg = v-30;
	  else if (v >= 40 && v <= 47) this.bg = v-40;
	  else if (v >= 90 && v <= 97) this.fg = (v-90)+8;
	  else if (v >= 100 && v <= 107) this.bg = (v-100)+8;
	  break;
      }
      pn.shift();
    }
  },

  normal : function(ch) {
    switch(ch) {
      case '\033':
	this.mode = 1;
	return;
      case '\007': return;
      case '\b': return this.left(1);
      case '\t': return this.tab();
      case '\n':
      case '\v':
      case '\f': return this.down(1,true);
      case '\r': return this.cr();
      default:
	if (ch < ' ' || ch == 127) return;
    }
    var m = this.m[this.cy][this.cx];
    m.char = ch;
    m.e.innerHTML = (ch == ' ')? "&nbsp;" : ch;

    if (this.reverse) this.setColor(m,this.colorInverse(this.fg), this.colorInverse(this.bg));
    else this.setColor(m, this.fg, this.bg);
    m.e.style.textDecoration = this.underline? "underline" : "initial";
    m.e.style.fontWeight = this.bold? "bold" : "normal";

    this.cx++;
    if (this.cx >= this.cols) {
      this.cx = 0;
      if (this.cy+1 >= this.rows) this.scrollup();
      else this.cy++;
    }
  },

  parsecq : function() {
    if (this.cq.length == 0) return [ 0, 0 ];
    var a = [0, 0], s = this.cq.split(";");
    for(var i = 0; i < s.length; i++) {
      a[i] = parseInt(s[i]);
    }
    this.cqlen = s.length;
    this.cq = "";
    return a;
  },
  cqreset : function() {
    this.cqlen = 0;
    this.cq = "";
  },

  setmode : function(pn) {
    for(var i = 0; i < this.cqlen; i++) {
      switch(pn[i]) {
	case 1: // application cursor keys
// 	case 2:	// keyboard lock mode
// 	case 4: // insert mode
// 	case 12: // disable local echo
// 	case 20: // LF,FF or VT moves to 1st column on next line (CR+LF)
      }
    }
  },
  resetmode : function(pn) {
  },

  qmode : function(ch) {
    if (ch >= '0' && ch <='9' || ch == ';') this.cq += ch;
    else {
      this.mode = 0;
      var pn = this.parsecq();
      switch(ch) {
	case 'l':
	  this.setmode(pn);
	  break;
	case 'h':
	  this.resetmode(pn);
	  break;
      }
    }
  },

  brac: function(ch) {
    if (ch >= '0' && ch <='9' || ch == ';') this.cq += ch;
    else {
      this.mode = 0;
      var pn = this.parsecq();
      switch(ch) {
	case 'A': // up
	  return this.up(pn[0], false);
	case 'B': // down
	  return this.down(pn[0], false);
	case 'C': // right
	  return this.right(pn[0]);
	case 'D': // left
	  return this.left(pn[0]);
	case 'f':
	case 'H':
	  return this.move(pn[0], pn[1]);
	case 'J':
	  return this.eid(pn[0]);
	case 'K':
	  return this.eil(pn[0]);
	case 'r':
	  return this.setMargins(pn[0], pn[1]);
	case 'm':
	  return this.setAttrs(pn);
	case '?':
	  if (this.cqlen == 0) this.mode = 3;
	  return;
	default:
      }
    }
  },

  escape : function(ch) {
    this.mode = 0;
    switch(ch) {
      case '[': return this.cqreset(), this.mode = 2;
      case '(': return this.cqreset(), this.mode = 3;
      case ')': return this.cqreset(), this.mode = 4;
      case 'c': return this.reset();
      case 'D': return this.down(1,true);
      case 'M': return this.up(1,true);
      case 'E':
	this.cr();
	this.down(1, true);
	return;
      case '7':
	this.sc = { cy : this.cy, cx : this.cx, cset : null };
	return;
      case '8':
	this.cx = this.sc.cx; this.cy = this.sc.cy;
	return;
      case '=':
	this.kbmode = true;
	return;
      case '>':
	this.kbmode = false;
	return;
      default:
    }
  },

  addch : function(ch) {
//    console.log(String.prototype.charCodeAt(ch));
    switch(this.mode) {
      case 3: return this.qmode(ch);
      case 2: return this.brac(ch);
      case 1: return this.escape(ch);
      case 0:
      default:
	this.normal(ch);
    }
  },

  addstr : function(s) {
//    console.log("]%s[", s);
    this.eraseCursor();
    for(var i = 0; i < s.length; i++) {
      this.addch(s[i]);
    }
    this.drawCursor();
  },

  loadState: function(k) {
    this.eraseCursor();
    this.cx = k.cx; this.cy = k.cy; this.sc = k.sc;
    this.mode = k.mode; this.cq = k.cq;
    this.tmargin = k.tmargin; this.bmargin = k.bmargin;
    this.fg = k.fg, this.bg = k.bg;
    this.bold = k.bold; this.underline = k.underline;
    this.blink = k.blink; this.reverse = k.reverse;

    var nm = k.m;
    for(var r = 0; r < this.rows; r++) {
      var ch = nm[r].c;
      var attr = nm[r].a;

      this.row[r].innerHTML = "";
      for(var c = 0; c < this.cols; c++) {
	var m = this.m[r][c];
	var td = (m.e = document.createElement("td"));
	td.innerHTML = ((m.char = ch[c]) == ' ')? "&nbsp;" : ch[c];

	this.setColor(m, Math.floor(attr[c] & this.FGMASK), Math.floor((attr[c] & this.BGMASK) >> 4));
	if (attr[c] & this.BOLD) td.style.fontWeight = "bold";
	if (attr[c] & this.UNDERLINE) td.style.textDecoration = "underline";
	this.row[r].appendChild(td);
      }
    }
    this.drawCursor();
  },

  reset : function() {
    this.homeRow = this.homeCol = this.cx = this.cy = this.mode = 0;
    this.sc = { cy : 0, cx: 0, charset: null };
    this.tmargin = 0; this.bmargin = this.rows-1;
    this.fg = 7; this.bg = 0;
    this.bold = this.underline = this.blink = this.reverse = false;

    this.m = [];
    this.row = [];

    this.window.innerHTML = "";
    for(var r = 0; r < this.rows; r++) {
      var nr = this.newrow();
      this.m[r] = nr.m;
      this.window.appendChild(this.row[r] = nr.row);
    }
    this.drawCursor();
  },
  init: function(w, h, scrollback) {
    this.rows = h;
    this.cols = w;
    this.enablescrollback = scrollback;
    this.window = document.getElementById("terminal");
    this.scrollback = document.getElementById("scrollback");
    this.tc = document.getElementById("tc");
    this.reset();
    tc.style.height = this.window.clientHeight+12 + "px";
    if (scrollback) this.addstr("01234567890123456789012345678901234567890123456789012345678901234567890123456789");
  },

  colorInverse : function(color) {
    if (color < 8) return 7-color;
    return 15-color;
  },
  setColor : function(m, f, b) {
    m.fg = f;
    m.bg = b;
    m.e.style.color = this.colors[f];
    m.e.style.backgroundColor = this.colors[b];
  },
  eraseCursor : function() {
    var m = this.m[this.cy][this.cx];
    m.e.style.color = this.colors[m.fg];
    m.e.style.backgroundColor = this.colors[m.bg];
  },
  drawCursor: function() {
    var m = this.m[this.cy][this.cx];
    m.e.style.color = this.colors[this.colorInverse(m.fg)];
    m.e.style.backgroundColor = this.colors[this.colorInverse(m.bg)];
  }
};