view shterm.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 9bef0941c6dd
line wrap: on
line source

// A state machine that keeps track of polling the server.
var ajaxstate = {
  state: 0,
  timerID: null,
  clear: function () {
    if (this.timerID != null) {
      window.clearTimeout(this.timerID);
      this.timerID = null;
    }
  },
  set: function (ms) {
    this.clear();
    this.timerID = window.setTimeout(getData, ms);
  },
  gotdata: function () {
    this.set(100);
    this.state = 0;
  },
  gotnothing: function () {
    if (this.state == 0) {
      this.set(100);
      this.state = 1;
    }
    else if (this.state == 1) {
      this.set(300);
      this.state = 2;
    }
    else if (this.state == 2) {
      this.set(1000);
      this.state = 3;
    }
    else {
      this.set(5000);
      this.state = 3;
    }
  },
  posted: function () {
    this.set(100);
    this.state = 0;
  }
};

function writeData(hexstr) {
  var codenum;
  var codes = [];
  var nc;
  var u8wait = 0; /* Stores bits from previous bytes of multibyte sequences. */
  var expect = 0; /* The number of 10------ bytes expected. */
  /* UTF-8 translation. */
  for (var i = 0; i < hexstr.length; i += 2) {
    nc = Number("0x" + hexstr.substr(i, 2));
    if (nc < 0x7F) {
      /* 0------- */
      codes.push(nc);
      /* Any incomplete sequence will be discarded. */
      u8wait = 0;
      expect = 0;
    }
    else if (nc < 0xC0) {
      /* 10------ : part of a multibyte sequence */
      if (expect > 0) {
        u8wait <<= 6;
        u8wait += (nc & 0x3F);
        expect--;
        if (expect == 0) {
          codes.push(u8wait);
          u8wait = 0;
        }
      }
      else {
        /* Assume an initial byte was missed. */
        u8wait = 0;
      }
    }
    /* These will all discard any incomplete sequence. */
    else if (nc < 0xE0) {
      /* 110----- : introduces 2-byte sequence */
      u8wait = (nc & 0x1F);
      expect = 1;
    }
    else if (nc < 0xF0) {
      /* 1110---- : introduces 3-byte sequence */
      u8wait = (nc & 0x0F);
      expect = 2;
    }
    else if (nc < 0xF8) {
      /* 11110--- : introduces 4-byte sequence */
      u8wait = (nc & 0x07);
      expect = 3;
    }
    else if (nc < 0xFC) {
      /* 111110-- : introduces 5-byte sequence */
      u8wait = (nc & 0x03);
      expect = 4;
    }
    else if (nc < 0xFE) {
      /* 1111110- : introduces 6-byte sequence */
      u8wait = (nc & 0x01);
      expect = 5;
    }
    else {
      /* 1111111- : should never appear */
      u8wait = 0;
      expect = 0;
    }
    /* Supporting all 31 bits is probably overkill... */
  }
  termemu.write(codes);
  return;
}

function getData() {
  if (!termemu.alive)
    return;
  var datareq = new XMLHttpRequest();
  datareq.onreadystatechange = function () {
    if (datareq.readyState == 4 && datareq.status == 200) {
      var datalines = datareq.responseText.split("\n");
      if (!datalines[0]) {
        return;
      }
      else if (datalines[0] == "E1") {
        termemu.alive = false;
        return;
      }
      else if (datalines[0].charAt(0) != 'd') {
        return;
      }
      if (datalines[1]) {
        writeData(datalines[1]);
        ajaxstate.gotdata();
      }
      else {
        ajaxstate.gotnothing();
      }
      return;
    }
  };
  datareq.open('GET', '/feed', true);
  datareq.send(null);
  return;
}

function postResponseHandler() {
  if (this.readyState == 4 && this.status == 200) {
    var datalines = this.responseText.split("\n");
    if (!datalines[0])
      return;
    else if (datalines[0] == "E1") {
      termemu.alive = false;
      return;
    }
    else if (datalines[0].charAt(0) != "d")
      return;
    /* It is a data message */
    if (datalines[1]) {
      writeData(datalines[1]);
    }
    ajaxstate.posted();
    return;
  }
}

function sendback(str) {
  /* For responding to terminal queries. */
  var datareq = new XMLHttpRequest();
  datareq.onreadystatechange = postResponseHandler;
  datareq.open('POST', '/feed', true);
  datareq.send("keys=" + str);
  return;
}

/* ASCII values of keys 0-9. */
var numShifts = [41, 33, 64, 35, 36, 37, 94, 38, 42, 40];

var keyHexCodes = {
  init: function () {
    this[KeyboardEvent.DOM_VK_RETURN] = ["0d", "0d"];
    this[KeyboardEvent.DOM_VK_SPACE] =  ["20", "20"];
    this[KeyboardEvent.DOM_VK_TAB] =    ["09", "09"];
    this[KeyboardEvent.DOM_VK_BACK_QUOTE] =    ["60", "7e"];
    this[KeyboardEvent.DOM_VK_OPEN_BRACKET] =  ["5b", "7b"];
    this[KeyboardEvent.DOM_VK_CLOSE_BRACKET] = ["5d", "7d"];
    this[KeyboardEvent.DOM_VK_BACK_SLASH] = ["5c", "7c"];
    this[KeyboardEvent.DOM_VK_SEMICOLON] =  ["3b", "3a"];
    this[KeyboardEvent.DOM_VK_QUOTE] =  ["27", "22"];
    this[KeyboardEvent.DOM_VK_COMMA] =  ["2c", "3c"];
    this[KeyboardEvent.DOM_VK_PERIOD] = ["2e", "3e"];
    this[KeyboardEvent.DOM_VK_SLASH] =  ["2f", "3f"];
    this[KeyboardEvent.DOM_VK_EQUALS] = ["3d", "2b"];
    this[KeyboardEvent.DOM_VK_SUBTRACT] =   ["2d", "5f"];
    this[KeyboardEvent.DOM_VK_BACK_SPACE] = ["08", "08"];
    this[KeyboardEvent.DOM_VK_ESCAPE] = ["1b", "1b"];
    this[KeyboardEvent.DOM_VK_PAGE_UP] =   ["1b5b357e", "1b5b357e"];
    this[KeyboardEvent.DOM_VK_PAGE_DOWN] = ["1b5b367e", "1b5b367e"];
    this.appCursor(false);
    this.appKeypad(false);
  },
  /* Multi-char control sequences!  Neat! */
  appCursor: function (on) {
    /* Aren't special keys vile? */
    if (on) {
      this[KeyboardEvent.DOM_VK_LEFT] =  ["1b4f44", "1b4f44"];
      this[KeyboardEvent.DOM_VK_RIGHT] = ["1b4f43", "1b4f43"];
      this[KeyboardEvent.DOM_VK_UP] =    ["1b4f41", "1b4f41"];
      this[KeyboardEvent.DOM_VK_DOWN] =  ["1b4f42", "1b4f42"];
      this[KeyboardEvent.DOM_VK_END] =   ["1b4f46", "1b4f46"];
      this[KeyboardEvent.DOM_VK_HOME] =  ["1b4f48", "1b4f48"];
    }
    else {
      this[KeyboardEvent.DOM_VK_LEFT] =  ["1b5b44", "1b5b44"];
      this[KeyboardEvent.DOM_VK_RIGHT] = ["1b5b43", "1b5b43"];
      this[KeyboardEvent.DOM_VK_UP] =    ["1b5b41", "1b5b41"];
      this[KeyboardEvent.DOM_VK_DOWN] =  ["1b5b42", "1b5b42"];
      this[KeyboardEvent.DOM_VK_END] =   ["1b5b46", "1b5b46"];
      this[KeyboardEvent.DOM_VK_HOME] =  ["1b5b48", "1b5b48"];
    }
  },
  appKeypad: function (on) {
    /* In theory, these should produce either numerals or the k[a-c][1-3]
     * sequences.  Since we can't count on the terminfo description actually
     * containing those sequences, pretend they're just arrow keys etc.
     */
    this[KeyboardEvent.DOM_VK_NUMPAD1] = ["1b4f46", "1b4f46"];
    this[KeyboardEvent.DOM_VK_NUMPAD2] = ["1b4f42", "1b4f42"];
    this[KeyboardEvent.DOM_VK_NUMPAD3] = ["1b5b367e", "1b5b367e"];
    this[KeyboardEvent.DOM_VK_NUMPAD4] = ["1b4f44", "1b4f44"];
    this[KeyboardEvent.DOM_VK_NUMPAD5] = ["1b5b45", "1b5b45"];
    this[KeyboardEvent.DOM_VK_NUMPAD6] = ["1b4f43", "1b4f43"];
    this[KeyboardEvent.DOM_VK_NUMPAD7] = ["1b4f48", "1b4f48"];
    this[KeyboardEvent.DOM_VK_NUMPAD8] = ["1b4f41", "1b4f41"];
    this[KeyboardEvent.DOM_VK_NUMPAD9] = ["1b5b357e", "1b5b357e"];
    return;
  }
};

function sendkey(ev) {
  var keynum = ev.keyCode;
  var code;
  if (keynum >= ev.DOM_VK_A && keynum <= ev.DOM_VK_Z) {
    /* Letters.  This assumes the codes are 65-90. */
    if (ev.ctrlKey)
      keynum -= 64;
    else if (!ev.shiftKey)
      keynum += 32;
    code = keynum.toString(16);
    if (code.length < 2)
      code = "0" + code;
  }
  else if (keynum >= ev.DOM_VK_0 && keynum <= ev.DOM_VK_9) {
    /* The number row. */
    if (ev.shiftKey) {
      code = numShifts[keynum - 48].toString(16);
    }
    else {
      code = keynum.toString(16);
    }
  }
  else if (keynum in keyHexCodes) {
    if (ev.shiftKey)
      code = keyHexCodes[keynum][1];
    else
      code = keyHexCodes[keynum][0];
  }
  else if (keynum == ev.DOM_VK_SHIFT || keynum == ev.DOM_VK_CONTROL ||
           keynum == ev.DOM_VK_ALT || keynum == ev.DOM_VK_CAPS_LOCK) {
    return;
  }
  else {
    debug(1, "Ignoring keycode " + keynum);
    return;
  }
  if (termemu.alive)
    ev.preventDefault();
  var datareq = new XMLHttpRequest();
  datareq.onreadystatechange = postResponseHandler;
  datareq.open('POST', '/feed', true);
  datareq.send("keys=" + code);
  //dkey(code);
  return;
}

var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c",
  ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e"
}

function vkey(c) {
  var keystr;
  if (c.match(/^[a-z]$/)) {
    if (termemu.ctrlp()) {
      var n = c.charCodeAt(0) - 96;
      keystr = n.toString(16);
      if (keystr.length < 2)
        keystr = "0" + keystr;
    }
    else if (termemu.shiftp())
      keystr = c.toUpperCase().charCodeAt(0).toString(16);
    else
      keystr = c.charCodeAt(0).toString(16);
  }
  else if (c.match(/^[0-9]$/)) {
    if (termemu.shiftp())
      keystr = numShifts[c.charCodeAt(0) - 48].toString(16);
    else
      keystr = c.charCodeAt(0).toString(16);
  }
  else if (c == '\n')
    keystr = "0a";
  else if (c == '\t')
    keystr = "09";
  else if (c == '\b')
    keystr = "08";
  else if (c == ' ')
    keystr = "20";
  else if (c in charshifts) {
    if (termemu.shiftp())
      keystr = charshifts[c];
    else
      keystr = c.charCodeAt(0).toString(16);
  }
  else
    return;
  //writeData("Sending " + keystr);
  var datareq = new XMLHttpRequest();
  datareq.onreadystatechange = postResponseHandler;
  datareq.open('POST', '/feed', true);
  datareq.send("keys=" + keystr);
  return;
}

function setup() {
  keyHexCodes.init();
  termemu.init("termwrap");
  setTitle("Not connected.");
  return;
}

function toggleshift() {
  termemu.toggleshift();
  keydiv = document.getElementById("shiftkey");
  if (termemu.shiftp())
    keydiv.className = "keysel";
  else
    keydiv.className = "key";
  return;
}

function togglectrl() {
  termemu.togglectrl();
  keydiv = document.getElementById("ctrlkey");
  if (termemu.ctrlp())
    keydiv.className = "keysel";
  else
    keydiv.className = "key";
  return;
}

function login() {
  if (termemu.alive)
    return;
  var req = new XMLHttpRequest();
  req.onreadystatechange = function () {
    if (req.readyState == 4 && req.status == 200) {
      var datalines = req.responseText.split("\n");
      if (datalines[0] == 'l1') {
        /* Success */
        termemu.alive = true;
	setTitle("Logged in");
        debug(1, "Logged in with id " + datalines[1]);
        getData();
        return;
      }
      return;
    }
  };
  req.open('POST', '/login', true);
  req.send("login=login");
  return;
}

function stop() {
  var req = new XMLHttpRequest();
  req.onreadystatechange = function () {
    if (req.readyState == 4 && req.status == 200) {
      var datalines = req.responseText.split("\n");
      /* Figure out whether or not it worked. */
      termemu.alive = false;
      return;
    }
  };
  req.open('POST', '/feed', true);
  req.send("quit=quit");
  return;
}

function setTitle(tstr) {
  var titlespan = document.getElementById("ttitle");
  var tnode = document.createTextNode(tstr);
  if (titlespan.childNodes.length == 0)
    titlespan.appendChild(tnode);
  else
    titlespan.replaceChild(tnode, titlespan.childNodes[0]);
  return;
}

function debug(level, msg) {
  if (level < debugSuppress)
    return;
  var msgdiv = document.createElement("div");
  var msgtext = document.createTextNode(msg);
  msgdiv.appendChild(msgtext);
  document.getElementById("debug").appendChild(msgdiv);
  return;
}

/* This should be a termemu method. */
function textsize(larger) {
  var cssSize = document.getElementById("term").style.fontSize;
  if (!cssSize)
    return;
  var match = cssSize.match(/\d*/);
  if (!match)
    return;
  var csize = Number(match[0]);
  var nsize;
  if (larger) {
    if (csize >= 48)
      nsize = 48;
    else if (csize >= 20)
      nsize = csize + 4;
    else if (csize >= 12)
      nsize = csize + 2;
    else if (csize >= 8)
      nsize = csize + 1;
    else
      nsize = 8;
  }
  else {
    if (csize <= 8)
      nsize = 8;
    else if (csize <= 12)
      nsize = csize - 1;
    else if (csize <= 20)
      nsize = csize - 2;
    else if (csize <= 48)
      nsize = csize - 4;
    else
      nsize = 48;
  }
  document.getElementById("term").style.fontSize = nsize.toString() + "px";
  termemu.resize();
  debug(1, "Changing font size to " + nsize.toString());
  return;
}

function bell(on) {
  var imgnode = document.getElementById("bell");
  if (on) {
    imgnode.style.visibility = "visible";
    window.setTimeout(bell, 1500, false);
  }
  else
    imgnode.style.visibility = "hidden";
  return;
}

function dkey(codestr) {
  var dstr = "Keystring: ";
  for (var i = 0; i < codestr.length; i += 2) {
    code = Number("0x" + codestr.substr(i, 2));
    if (code < 32 || (code >= 127 && code < 160))
      dstr += "\\x" + code.toString(16);
    else
      dstr += String.fromCharCode(code);
  }
  debug(1, dstr);
  return;
}