# HG changeset patch # User John "Elwin" Edwards # Date 1336319140 25200 # Node ID bd412f63ce0de0d06c209d072841e2be7cfc4fbd Put this project under version control, finally. diff -r 000000000000 -r bd412f63ce0d bell.svg --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bell.svg Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,72 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff -r 000000000000 -r bd412f63ce0d index-rlg.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/index-rlg.html Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,103 @@ + + + +WebTTY + + + + + +

WebTTY

+
+ + bell +
+
TERM
+
+
`
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
0
+
-
+
=
+
Bksp
+
+
+
Tab
+
Q
+
W
+
E
+
R
+
T
+
Y
+
U
+
I
+
O
+
P
+
[
+
]
+
\
+
+
+
Ctrl
+
A
+
S
+
D
+
F
+
G
+
H
+
J
+
K
+
L
+
;
+
'
+
Ret
+
+
+
Shift
+
Z
+
X
+
C
+
V
+
B
+
N
+
M
+
,
+
.
+
/
+
+
+
+
+
Stop
+
Font: +Smaller +Larger +
+
+
+
+Name: +Password: +Choose game: + +
+
+
+
+

Debugging Output

+
+ + diff -r 000000000000 -r bd412f63ce0d index-sh.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/index-sh.html Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,91 @@ + + + +WebTTY + + + + + +

WebTTY

+
+ + bell +
+
+Browsing with Javascript turned off? I sympathize. I didn't want Javascript to be necessary for WebTerm. Unfortunately, the only other way to make it work was Java applets. +
+
+
`
+
1
+
2
+
3
+
4
+
5
+
6
+
7
+
8
+
9
+
0
+
-
+
=
+
Bksp
+
+
+
Tab
+
Q
+
W
+
E
+
R
+
T
+
Y
+
U
+
I
+
O
+
P
+
[
+
]
+
\
+
+
+
Ctrl
+
A
+
S
+
D
+
F
+
G
+
H
+
J
+
K
+
L
+
;
+
'
+
Ret
+
+
+
Shift
+
Z
+
X
+
C
+
V
+
B
+
N
+
M
+
,
+
.
+
/
+
+
+
+
+
Log in
+
Stop
+
Font: +Smaller +Larger +
+
+

Debugging Output

+
+ + diff -r 000000000000 -r bd412f63ce0d ptyhelper.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ptyhelper.c Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int got_sighup = 0; + +void handle_HUP(int signum) { + if (signum == SIGHUP) + got_sighup = 1; + return; +} + +int main(int argc, char *argv[]) { + + int ptymaster, ptyslave; /* File descriptors */ + int child; + int status, selstatus; + int w = 80, h = 24, t; + struct sigaction sighup_act; + fd_set readset; + struct timeval select_time; + char buf[4096]; + int nread; + char *penv, *ptmp; +#if 0 + struct termios ptysettings; +#endif + struct winsize ptysize; + + if (argc == 1) { + fprintf(stderr, "No command given.\n"); + exit(1); + } + + /* Set up the signal handler. */ + sighup_act.sa_handler = &handle_HUP; + sighup_act.sa_flags = SA_RESTART; + sigaction(SIGHUP, &sighup_act, NULL); + + /* Check the environment for configuration options. */ + penv = getenv("PTYHELPER"); + if (penv != NULL) { + t = strtol(penv, &ptmp, 10); + if (t > 0 && t < 256) + h = t; + if (*ptmp != '\0') { + penv = ptmp + 1; + t = strtol(penv, &ptmp, 10); + if (t > 0 && t < 256) + w = t; + } + } + /* Set up the size. */ + ptysize.ws_row = h; + ptysize.ws_col = w; + + /* Open a pty */ + if (openpty(&ptymaster, &ptyslave, NULL, NULL, &ptysize)) { + return 1; + } +#if 0 + /* Put it into raw mode. */ + tcgetattr(ptyslave, &ptysettings); + cfmakeraw(&ptysettings); + tcsetattr(ptyslave, TCSANOW, &ptysettings); +#endif + + /* Start the child */ + /* forkpty() might be more convenient. */ + if (!(child = fork())) { + /* Child process */ + login_tty(ptyslave); + close(ptymaster); + execvp(argv[1], argv + 1); + perror("execvp() failed"); + return 1; + } + close(ptyslave); + + while (1) { + /* Now do a select() over stdin and ptymaster, and write anything that + * appears to ptymaster and stdout respectively. */ + FD_ZERO(&readset); + FD_SET(0, &readset); + FD_SET(ptymaster, &readset); + select_time.tv_sec = 1; + select_time.tv_usec = 0; + selstatus = select(ptymaster + 1, &readset, NULL, NULL, &select_time); + if (selstatus > 0) { + /* TODO make sure it all gets written if a signal interrupts write(). */ + if (FD_ISSET(0, &readset)) { + nread = read(0, buf, 4096); + if (nread > 0) { + write(ptymaster, buf, nread); + } + } + if (FD_ISSET(ptymaster, &readset)) { + nread = read(ptymaster, buf, 4096); + if (nread > 0) { + write(1, buf, nread); + } + } + } + + /* Periodically check to see if we're done. */ + /* TODO: catch SIGCHLD and only wait() if it is delivered. */ + if (waitpid(child, &status, WNOHANG)) { + break; + } + + /* If node sighup's us, pass it along. */ + if (got_sighup) { + kill(child, SIGHUP); + } + } + + /* Get any leftover output and clean up. */ + /* FIXME looping over select() is pointless if there's only one fd that + * nothing's writing to. Just loop over read() until it's empty. */ + while (1) { + FD_ZERO(&readset); + FD_SET(ptymaster, &readset); + select_time.tv_sec = 0; + select_time.tv_usec = 0; + if (select(ptymaster + 1, &readset, NULL, NULL, &select_time) > 0) { + nread = read(ptymaster, buf, 4096); + if (nread > 0) { + write(1, buf, nread); + } + else + break; + } + else + break; + } + close(ptymaster); + + /* Return the child's exit status. */ + if (WIFEXITED(status)) + return WEXITSTATUS(status); + return 0; +} diff -r 000000000000 -r bd412f63ce0d quickrypt.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/quickrypt.c Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,28 @@ +#include +#include +#include +#include + +int main(int argc, char *argv[]) { + char clear[32], enc[120], *ptr; + fgets(&clear, 32, stdin); + if (!(ptr = strchr(&clear, '\n'))) + return 1; + else + *ptr = '\0'; + fgets(&enc, 120, stdin); + if (!(ptr = strchr(&enc, '\n'))) + return 1; + else + *ptr = '\0'; + ptr = crypt(clear, enc); + if (!strcmp(argv[argc - 1], "-s")) { + /* Option -s for "show": output the encrypted version. */ + printf("%s\n", ptr); + return 0; + } + /* Otherwise this is a check. */ + else if (!strcmp(ptr, enc)) + return 0; + return 1; +} diff -r 000000000000 -r bd412f63ce0d rlgterm.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rlgterm.js Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,485 @@ +/* rlgterm.js: Roguelike Gallery's driver for termemu.js */ + +// 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; +} + +/* Processes a message from the server, returning true or false if it was a + * data message with or without data, null if not data. */ +function processMsg(msg) { + var msglines = msg.split("\n"); + var havedata = null; + if (!msglines[0]) + return null; + if (msglines[0].charAt(0) == 'd') { + if (msglines[1]){ + writeData(msglines[1]); + havedata = true; + } + else { + havedata = false; + } + } + else if (msglines[0] == "E1") { + logout(); + } + else if (msglines[0].charAt(0) == "T") { + setTitle(msglines[1]); + } + else if (msglines[0] == "q1") { + logout(); + } + else { + debug(1, "Unrecognized server message " + msglines[0]); + } + return havedata; +} + +function getData() { + if (termemu.sessid == null) + return; + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = function () { + if (datareq.readyState == 4 && datareq.status == 200) { + var wasdata = processMsg(datareq.responseText); + if (wasdata != null) { + if (wasdata) + ajaxstate.gotdata(); + else + ajaxstate.gotnothing(); + } + return; + } + }; + datareq.open('POST', '/feed', true); + datareq.send("id=" + termemu.sessid); + return; +} + +function postResponseHandler() { + if (this.readyState == 4 && this.status == 200) { + // We might want to do something with wasdata someday. + var wasdata = processMsg(this.responseText); + 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("id=" + termemu.sessid + "&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"]; + /* Multi-char control sequences! Neat! */ + this[KeyboardEvent.DOM_VK_PAGE_UP] = ["1b5b357e", "1b5b357e"]; + this[KeyboardEvent.DOM_VK_PAGE_DOWN] = ["1b5b367e", "1b5b367e"]; + this.appCursor(false); + this.appKeypad(false); + }, + appCursor: function (on) { + 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) { + if (termemu.sessid == null) + return; + 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, NOT the numpad. */ + 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.sessid != null) + ev.preventDefault(); + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = postResponseHandler; + datareq.open('POST', '/feed', true); + datareq.send("id=" + termemu.sessid + "&keys=" + code); + return; +} + +var charshifts = { '-': "5f", '=': "2b", '[': "7b", ']': "7d", '\\': "7c", + ';': "3a", '\'': "22", ',': "3c", '.': "3e", '/': "3f", '`': "7e" +} + +function vkey(c) { + if (termemu.sessid == null) + 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 + return; + //writeData("Sending " + keystr); + var datareq = new XMLHttpRequest(); + datareq.onreadystatechange = postResponseHandler; + datareq.open('POST', '/feed', true); + datareq.send("id=" + termemu.sessid + "&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 formlogin(ev) { + ev.preventDefault(); + if (termemu.sessid != null) + return; + var formname = document.getElementById("input_name").value; + var formpass = document.getElementById("input_pw").value; + var formgame = document.getElementById("input_game").value; + var formdata = "game=" + encodeURIComponent(formgame) + "&name=" + encodeURIComponent(formname) + "&pw=" + encodeURIComponent(formpass); + 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.sessid = datalines[1]; + setTitle("Playing as " + formname); + debug(1, "Logged in with id " + termemu.sessid); + document.getElementById("loginform").style.display = "none"; + getData(); + } + else { + debug(1, "Could not start game: " + datalines[1]); + document.getElementById("input_name").value = ""; + document.getElementById("input_pw").value = ""; + } + } + }; + req.open('POST', '/login', true); + req.send(formdata); + return; +} + +function logout() { + if (termemu.sessid == null) + return; + termemu.sessid = null; + setTitle("Game over."); + document.getElementById("loginform").style.display = "block"; + return; +} + +function stop() { + var req = new XMLHttpRequest(); + req.onreadystatechange = function () { + if (req.readyState == 4 && req.status == 200) { + processMsg(req.responseText); + return; + } + }; + req.open('POST', '/feed', true); + req.send("id=" + termemu.sessid + "&quit=quit"); + 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; +} + +function textsize(larger) { + var cssSize = termemu.view.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; +} diff -r 000000000000 -r bd412f63ce0d shterm.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/shterm.js Sun May 06 08:45:40 2012 -0700 @@ -0,0 +1,478 @@ +// 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; +} diff -r 000000000000 -r bd412f63ce0d termemu.js --- /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) { + debug(1, "Unrecognized CSI sequence: " + comstr); + return; + } + var prefix = null; + if (matchCSI[1]) + prefix = matchCSI[1]; + var postfix = null; + if (matchCSI[3]) + postfix = matchCSI[3]; + var params = []; + if (matchCSI[2]) { + var numstrs = matchCSI[2].split(";"); + for (var i = 0; i < numstrs.length; i++) { + if (numstrs[i]) + params.push(Number(numstrs[i])); + else + params.push(null); + } + } + /* Useful if expecting a single parameter which is a count. */ + var count = 1; + if (params[0]) + count = params[0];