view shterm.js @ 145:3308eaa00c91

Use SVG for the bell icon instead of PNG. Browsers that support WebSockets tend to support SVG too. This also removes the need to install Inkscape to convert the file to PNG.
author John "Elwin" Edwards
date Thu, 31 Oct 2013 20:44:35 -0700
parents 789c094675f4
children c4a32007d2dc
line wrap: on
line source

/* shterm.js: browser-side JavaScript to handle I/O for termemu.js when it 
 * is running a shell via the webtty.js server.
 */

var isalive = false; // Whether the session is currently active.
var nsend = 0; // The number of the next packet to send.
var nrecv = 0; // The next packet expected.
var msgQ = []; // Queue for out-of-order messages.
var conn = null; // WebSocket

// 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 processMsg(response) {
  if (response.t != "d" || typeof(response.d) != "string")
    return;
  if (response.n === nrecv) {
    writeData(response.d);
    nrecv++;
    var next;
    /* msgQ must be shifted every time nrecv is incremented, but the process
     * stops whenever an empty space, corresponding to an unarrived message,
     * is encountered.
     */
    while ((next = msgQ.shift()) !== undefined) {
      writeData(next.d);
      nrecv++;
    }
  }
  else if (response.n > nrecv) {
    /* The current message comes after one still missing.  Queue this one
     * for later use.
     */
    debug(1, "Got packet " + response.n + ", expected " + nrecv);
    msgQ[response.n - nrecv - 1] = response;
  }
  else {
    /* This message's number was encountered previously. */
    debug(1, "Discarding packet " + response.n + ", expected " + nrecv);
  }
}

function getData() {
  if (!isalive)
    return;
  var datareq = new XMLHttpRequest();
  datareq.onreadystatechange = function () {
    if (datareq.readyState == 4 && datareq.status == 200) {
      var response = JSON.parse(this.responseText);
      if (!response.t)
        return;
      else if (response.t == "E") {
        if (response.c == 1) {
          isalive = false;
          debug(1, "Server error: " + response.s);
        }
      }
      else if (response.t == "n")
        ajaxstate.gotnothing();
      else if (response.t == "d") {
        processMsg(response);
        ajaxstate.gotdata();
      }
      return;
    }
  };
  datareq.open('GET', '/feed', true);
  datareq.send(null);
  return;
}

function postResponseHandler() {
  if (this.readyState == 4 && this.status == 200) {
    var response = JSON.parse(this.responseText);
    if (!response.t || response.t == "n")
      return;
    else if (response.t == "E") {
      if (response.c == 1) {
        isalive = false;
        debug(1, "Server error: " + response.s);
      }
      return;
    }
    else if (response.t != "d")
      return;
    /* It is a data message */
    if (response.d) {
      processMsg(response);
      //debug(1, "Got packet " + response.n);
    }
    ajaxstate.posted();
    return;
  }
}

function sendback(str) {
  /* For responding to terminal queries. */
  if (conn) {
    var msgObj = {"t": "d", "d": str};
    conn.send(JSON.stringify(msgObj));
  }
  else {
    var formdata = {"t": "d", "n": nsend++, "d": str};
    var datareq = new XMLHttpRequest();
    datareq.onreadystatechange = postResponseHandler;
    datareq.open('POST', '/feed', true);
    datareq.send(JSON.stringify(formdata));
  }
  return;
}

function sendkey(ev) {
  if (!isalive)
    return;
  var keynum = ev.keyCode;
  var code;
  if (keynum >= 65 && keynum <= 90) {
    /* Letters. */
    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 >= 48 && keynum <= 57) {
    /* 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 >= 16 && keynum <= 20) {
    return;
  }
  else {
    debug(1, "Ignoring keycode " + keynum);
    return;
  }
  ev.preventDefault();
  if (conn) {
    var msgObj = {"t": "d", "d": code};
    conn.send(JSON.stringify(msgObj));
  }
  else {
    var formdata = {"t": "d", "n": nsend++, "d": code};
    var datareq = new XMLHttpRequest();
    datareq.onreadystatechange = postResponseHandler;
    datareq.open('POST', '/feed', true);
    datareq.send(JSON.stringify(formdata));
  }
  //dkey(code);
  return;
}

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

var kpkeys = { "KP1": "1b4f46", "KP2": "1b4f42", "KP3": "1b5b367e", 
               "KP4": "1b4f44", "KP5": "1b5b45", "KP6": "1b4f43", 
               "KP7": "1b4f48", "KP8": "1b4f41", "KP9": "1b5b357e" };

function vkey(c) {
  if (!isalive)
    return;
  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 if (c in kpkeys) {
    keystr = kpkeys[c];
  }
  else
    return;
  //writeData("Sending " + keystr);
  if (conn) {
    var msgObj = {"t": "d", "d": keystr};
    conn.send(JSON.stringify(msgObj));
  }
  else {
    var formdata = {"t": "d", "n": nsend++, "d": keystr};
    var datareq = new XMLHttpRequest();
    datareq.onreadystatechange = postResponseHandler;
    datareq.open('POST', '/feed', true);
    datareq.send(JSON.stringify(formdata));
  }
  return;
}

function setup() {
  keyHexCodes.init();
  termemu.init("termwrap", 24, 80);
  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 loginWS(h, w) {
  if (conn)
    return;
  var sockurl = "ws://" + window.location.host + "/sock?w=" + w + "&h=" + h;
  conn = new WebSocket(sockurl);
  conn.onopen = function (event) {
    isalive = true;
    setTitle("Logged in");
    debug(1, "Logged in via WebSocket");
  }
  conn.onmessage = function (event) {
    var msgObj = JSON.parse(event.data);
    if (msgObj.t == 'l') {
      termemu.resize(msgObj.h, msgObj.w);
    }
    else if (msgObj.t == 'd') {
      debug(0, msgObj.d);
      writeData(msgObj.d);
    }
    else if (msgObj.t == 'q') {
      debug(0, "Quit message!");
      conn.close();
    }
  }
  conn.onclose = function (event) {
    conn = null;
    isalive = false;
    debug(1, "WebSocket connection closed.");
    setTitle("Not connected.");
  }
}

function login(h, w) {
  if (isalive)
    return;
  if (window.WebSocket) {
    loginWS(h, w);
    return;
  }
  params = {"login": true, "h": h, "w": w};
  var req = new XMLHttpRequest();
  req.onreadystatechange = function () {
    if (req.readyState == 4 && req.status == 200) {
      var logindict = JSON.parse(req.responseText);
      if (logindict.login) {
        /* Success */
        termemu.resize(logindict.h, logindict.w);
        isalive = true;
        nsend = 0;
        nrecv = 0;
	setTitle("Logged in");
        debug(1, "Logged in with id " + logindict.id);
        getData();
        return;
      }
      return;
    }
  };
  req.open('POST', '/login', true);
  req.send(JSON.stringify(params));
  //req.send("login=login&h=" + String(h) + "&w=" + String(w));
  return;
}

function stop() {
  if (conn) {
    conn.close();
    return;
  }
  var req = new XMLHttpRequest();
  req.onreadystatechange = function () {
    if (req.readyState == 4 && req.status == 200) {
      /* Figure out whether or not it worked. */
      /* FIXME the server might respond with output. */
      isalive = false;
      return;
    }
  };
  req.open('POST', '/feed', true);
  req.send(JSON.stringify({"t": "q", "n": nsend++}));
  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.fixsize();
  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;
}