529 lines
13 KiB
JavaScript
529 lines
13 KiB
JavaScript
/* 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;
|
|
}
|