changeset 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
files bell.svg index-rlg.html index-sh.html ptyhelper.c quickrypt.c rlgterm.js shterm.js termemu.js tty.css webtty.js webttyd.js
diffstat 11 files changed, 3461 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="16"
+   height="16"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.1 r9760"
+   sodipodi:docname="New document 1">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.2"
+     inkscape:cx="17.844259"
+     inkscape:cy="5.9285714"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="873"
+     inkscape:window-height="546"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="0" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1036.3622)">
+    <path
+       style="fill:none;stroke:#000000;stroke-linejoin:round;stroke-opacity:1"
+       d="m 4.2931485,1040.1868 c 0.8310158,-1.1611 2.3601954,-2 3.7880723,-2 1.4278768,0 2.9570562,0.8389 3.7880722,2 1.238121,1.73 0.404997,4.2608 1,6.3033 0.234101,0.8036 1,2.3033 1,2.3033 l -11.5761445,0 c 0,0 0.7658995,-1.4997 1,-2.3033 0.5950026,-2.0425 -0.2381208,-4.5733 1,-6.3033 z"
+       id="rect2985"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ssssccss" />
+    <path
+       sodipodi:type="arc"
+       style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+       id="path3762"
+       sodipodi:cx="11.696428"
+       sodipodi:cy="14.660714"
+       sodipodi:rx="1.25"
+       sodipodi:ry="1.25"
+       d="m 12.946428,14.660714 a 1.25,1.25 0 1 1 -2.5,0 1.25,1.25 0 1 1 2.5,0 z"
+       transform="translate(-1.25,1035.4693)" />
+  </g>
+</svg>
--- /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 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>WebTTY</title>
+<script type="text/javascript" src="termemu.js"></script>
+<script type="text/javascript" src="rlgterm.js"></script>
+<link rel="stylesheet" type="text/css" href="tty.css">
+</head>
+<body onload="setup()" onkeydown="sendkey(event)">
+<h1>WebTTY</h1>
+<div id ="top">
+  <span id="ttitle"></span>
+  <img src="/bell.png" alt="bell" id="bell">
+</div>
+<div id="termwrap">TERM</div>
+<div class="keyrow">
+  <div class="key" onclick="vkey('`')">`</div>
+  <div class="key" onclick="vkey('1')">1</div>
+  <div class="key" onclick="vkey('2')">2</div>
+  <div class="key" onclick="vkey('3')">3</div>
+  <div class="key" onclick="vkey('4')">4</div>
+  <div class="key" onclick="vkey('5')">5</div>
+  <div class="key" onclick="vkey('6')">6</div>
+  <div class="key" onclick="vkey('7')">7</div>
+  <div class="key" onclick="vkey('8')">8</div>
+  <div class="key" onclick="vkey('9')">9</div>
+  <div class="key" onclick="vkey('0')">0</div>
+  <div class="key" onclick="vkey('-')">-</div>
+  <div class="key" onclick="vkey('=')">=</div>
+  <div class="key" onclick="vkey('\b')" style="width: 2.5em">Bksp</div>
+</div>
+<div class="keyrow">
+  <div class="key" onclick="vkey('\t')" style="width: 2.5em">Tab</div>
+  <div class="key" onclick="vkey('q')">Q</div>
+  <div class="key" onclick="vkey('w')">W</div>
+  <div class="key" onclick="vkey('e')">E</div>
+  <div class="key" onclick="vkey('r')">R</div>
+  <div class="key" onclick="vkey('t')">T</div>
+  <div class="key" onclick="vkey('y')">Y</div>
+  <div class="key" onclick="vkey('u')">U</div>
+  <div class="key" onclick="vkey('i')">I</div>
+  <div class="key" onclick="vkey('o')">O</div>
+  <div class="key" onclick="vkey('p')">P</div>
+  <div class="key" onclick="vkey('[')">[</div>
+  <div class="key" onclick="vkey(']')">]</div>
+  <div class="key" onclick="vkey('\\')">\</div>
+</div>
+<div class="keyrow">
+  <div class="key" onclick="togglectrl()" id="ctrlkey">Ctrl</div>
+  <div class="key" onclick="vkey('a')">A</div>
+  <div class="key" onclick="vkey('s')">S</div>
+  <div class="key" onclick="vkey('d')">D</div>
+  <div class="key" onclick="vkey('f')">F</div>
+  <div class="key" onclick="vkey('g')">G</div>
+  <div class="key" onclick="vkey('h')">H</div>
+  <div class="key" onclick="vkey('j')">J</div>
+  <div class="key" onclick="vkey('k')">K</div>
+  <div class="key" onclick="vkey('l')">L</div>
+  <div class="key" onclick="vkey(';')">;</div>
+  <div class="key" onclick="vkey('\'')">'</div>
+  <div class="key" onclick="vkey('\n')" style="width: 4em">Ret</div>
+</div>
+<div class="keyrow">
+  <div class="key" onclick="toggleshift()" id="shiftkey">Shift</div>
+  <div class="key" onclick="vkey('z')">Z</div>
+  <div class="key" onclick="vkey('x')">X</div>
+  <div class="key" onclick="vkey('c')">C</div>
+  <div class="key" onclick="vkey('v')">V</div>
+  <div class="key" onclick="vkey('b')">B</div>
+  <div class="key" onclick="vkey('n')">N</div>
+  <div class="key" onclick="vkey('m')">M</div>
+  <div class="key" onclick="vkey(',')">,</div>
+  <div class="key" onclick="vkey('.')">.</div>
+  <div class="key" onclick="vkey('/')">/</div>
+</div>
+<div class="keyrow">
+  <div class="key" onclick="vkey(' ')" id="spacebar"></div>
+</div>
+<div class="rbutton" onclick="stop()">Stop</div>
+<div class="rbutton">Font:
+<span onclick="textsize(false)">Smaller</span>
+<span onclick="textsize(true)">Larger</span>
+</div>
+<div>
+<form id="loginform" action="/login" method="post">
+<div>
+Name: <input type="text" name="name" id="input_name"> 
+Password: <input type="password" name="pw" id="input_pw">
+Choose game: <select name="game" id="input_game">
+  <option label="Rogue V3" value="rogue3">Rogue V3</option>
+  <option label="Rogue V4" value="rogue4">Rogue V4</option>
+  <option label="Rogue V5" value="rogue5">Rogue V5</option>
+  <option label="Super-Rogue" value="srogue">Super-Rogue</option>
+</select>
+<input type="submit" value="Play" onclick="formlogin(event)">
+</div>
+</form>
+</div>
+<div id="debug">
+<p>Debugging Output</p>
+</div>
+</body>
+</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 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+<title>WebTTY</title>
+<script type="text/javascript" src="termemu.js"></script>
+<script type="text/javascript" src="shterm.js"></script>
+<link rel="stylesheet" type="text/css" href="tty.css">
+</head>
+<body onload="setup()" onkeydown="sendkey(event)">
+<h1>WebTTY</h1>
+<div id ="top">
+  <span id="ttitle"></span>
+  <img src="/bell.png" alt="bell" id="bell">
+</div>
+<div id="termwrap">
+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.
+</div>
+<div class="keyrow">
+  <div class="key" onclick="vkey('`')">`</div>
+  <div class="key" onclick="vkey('1')">1</div>
+  <div class="key" onclick="vkey('2')">2</div>
+  <div class="key" onclick="vkey('3')">3</div>
+  <div class="key" onclick="vkey('4')">4</div>
+  <div class="key" onclick="vkey('5')">5</div>
+  <div class="key" onclick="vkey('6')">6</div>
+  <div class="key" onclick="vkey('7')">7</div>
+  <div class="key" onclick="vkey('8')">8</div>
+  <div class="key" onclick="vkey('9')">9</div>
+  <div class="key" onclick="vkey('0')">0</div>
+  <div class="key" onclick="vkey('-')">-</div>
+  <div class="key" onclick="vkey('=')">=</div>
+  <div class="key" onclick="vkey('\b')" style="width: 2.5em">Bksp</div>
+</div>
+<div class="keyrow">
+  <div class="key" onclick="vkey('\t')" style="width: 2.5em">Tab</div>
+  <div class="key" onclick="vkey('q')">Q</div>
+  <div class="key" onclick="vkey('w')">W</div>
+  <div class="key" onclick="vkey('e')">E</div>
+  <div class="key" onclick="vkey('r')">R</div>
+  <div class="key" onclick="vkey('t')">T</div>
+  <div class="key" onclick="vkey('y')">Y</div>
+  <div class="key" onclick="vkey('u')">U</div>
+  <div class="key" onclick="vkey('i')">I</div>
+  <div class="key" onclick="vkey('o')">O</div>
+  <div class="key" onclick="vkey('p')">P</div>
+  <div class="key" onclick="vkey('[')">[</div>
+  <div class="key" onclick="vkey(']')">]</div>
+  <div class="key" onclick="vkey('\\')">\</div>
+</div>
+<div class="keyrow">
+  <div class="key" onclick="togglectrl()" id="ctrlkey">Ctrl</div>
+  <div class="key" onclick="vkey('a')">A</div>
+  <div class="key" onclick="vkey('s')">S</div>
+  <div class="key" onclick="vkey('d')">D</div>
+  <div class="key" onclick="vkey('f')">F</div>
+  <div class="key" onclick="vkey('g')">G</div>
+  <div class="key" onclick="vkey('h')">H</div>
+  <div class="key" onclick="vkey('j')">J</div>
+  <div class="key" onclick="vkey('k')">K</div>
+  <div class="key" onclick="vkey('l')">L</div>
+  <div class="key" onclick="vkey(';')">;</div>
+  <div class="key" onclick="vkey('\'')">'</div>
+  <div class="key" onclick="vkey('\n')" style="width: 4em">Ret</div>
+</div>
+<div class="keyrow">
+  <div class="key" onclick="toggleshift()" id="shiftkey">Shift</div>
+  <div class="key" onclick="vkey('z')">Z</div>
+  <div class="key" onclick="vkey('x')">X</div>
+  <div class="key" onclick="vkey('c')">C</div>
+  <div class="key" onclick="vkey('v')">V</div>
+  <div class="key" onclick="vkey('b')">B</div>
+  <div class="key" onclick="vkey('n')">N</div>
+  <div class="key" onclick="vkey('m')">M</div>
+  <div class="key" onclick="vkey(',')">,</div>
+  <div class="key" onclick="vkey('.')">.</div>
+  <div class="key" onclick="vkey('/')">/</div>
+</div>
+<div class="keyrow">
+  <div class="key" onclick="vkey(' ')" id="spacebar"></div>
+</div>
+<div class="rbutton" onclick="login()">Log in</div>
+<div class="rbutton" onclick="stop()">Stop</div>
+<div class="rbutton">Font:
+<span onclick="textsize(false)">Smaller</span>
+<span onclick="textsize(true)">Larger</span>
+</div>
+<div id="debug">
+<p>Debugging Output</p>
+</div>
+</body>
+</html>
--- /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 <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <signal.h>
+#include <pty.h>
+#include <utmp.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <sys/select.h>
+#include <termios.h>
+
+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;
+}
--- /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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <crypt.h>
+
+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;
+}
--- /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;
+}
--- /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;
+}
--- /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];
+    /* The final character determines the action. */
+    if (c == 64) {
+      /* @ - insert spaces at cursor */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI @ sequence: " + comstr);
+        return;
+      }
+      /* The cursor stays still, but characters move out from under it. */
+      this.flipCursor();
+      var rowdiv = this.screen.childNodes[this.c.y];
+      var newspace;
+      while (count > 0) {
+        newspace = this.makeCell(' ');
+        rowdiv.insertBefore(newspace, rowdiv.childNodes[this.c.x]);
+        rowdiv.removeChild(rowdiv.lastChild);
+        count--;
+      }
+      /* Finally, put the cursor back. */
+      this.flipCursor();
+    }
+    else if (c >= 65 && c <= 71) {
+      /* A - up, B - down, C - forward, D - backward */
+      /* E - next line, F - previous line, G - to column */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI sequence: " + comstr);
+        return;
+      }
+      /* These may be out of range, but cmove will take care of that. */
+      if (c == 65)
+        this.cmove(this.c.y - count, null);
+      else if (c == 66)
+        this.cmove(this.c.y + count, null);
+      else if (c == 67)
+        this.cmove(null, this.c.x + count);
+      else if (c == 68)
+        this.cmove(null, this.c.x - count);
+      else if (c == 69)
+        this.cmove(this.c.y + count, 0);
+      else if (c == 70)
+        this.cmove(this.c.y - count, 0);
+      else if (c == 71)
+        this.cmove(null, count - 1);
+    }
+    else if (c == 72) {
+      /* H - move */
+      var x = 0;
+      var y = 0;
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI H sequence: " + comstr);
+        return;
+      }
+      if (params[0])
+        y = params[0] - 1;
+      if (params[1])
+        x = params[1] - 1;
+      if (y > 23)
+        y = 23;
+      if (x > 79)
+        x = 79;
+      debug(0, "Moving to row " + y + ", col " + x);
+      this.cmove(y, x);
+    }
+    else if (c == 73) {
+      /* I - move forward by tabs */
+      var x = this.c.x;
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI I sequence: " + comstr);
+        return;
+      }
+      while (count > 0) {
+        x = 8 * (Math.floor(x / 8) + 1);
+        if (x > 79) {
+          x = 79;
+          break;
+        }
+        count--;
+      }
+      this.cmove(null, x);
+    }
+    else if (c == 74) {
+      /* J - erase display */
+      var start;
+      var end;
+      var cols;
+      if (prefix == '?')
+        debug(1, "Warning: CSI ?J not implemented");
+      else if (prefix || postfix) {
+        debug(1, "Invalid CSI J sequence: " + comstr);
+        return;
+      }
+      if (!params[0]) {
+        /* Either 0 or not given */
+        start = this.c.y + 1;
+        end = 23;
+        cols = 1;
+      }
+      else if (params[0] == 1) {
+        start = 0;
+        end = this.c.y - 1;
+        cols = -1;
+      }
+      else if (params[0] == 2) {
+        start = 0;
+        end = 23;
+        cols = 0;
+      }
+      else {
+        debug(1, "Unimplemented parameter in CSI J sequence: " + comstr);
+        return;
+      }
+      for (var nrow = start; nrow <= end; nrow++) {
+        this.screen.replaceChild(this.makeRow(), this.screen.childNodes[nrow]);
+      }
+      if (cols != 0) {
+        /* Otherwise, the whole screen was erased and the active row doesn't
+         * need special treatment. */
+        var cursrow = this.screen.childNodes[this.c.y];
+        for (var ncol = this.c.x; ncol >= 0 && ncol < 80; ncol += cols) {
+          cursrow.replaceChild(this.makeCell(' '), cursrow.childNodes[ncol]);
+        }
+      }
+      this.offedge = false;
+      /* Always flip after replacing the active position. */
+      this.flipCursor();
+    }
+    else if (c == 75) {
+      /* K - erase line */
+      /* The ? is for an erase method that respects an "uneraseable" attribute,
+       * which isn't implemented, so the methods are equivalent for now. */
+      var start;
+      var end;
+      if (prefix == '?')
+        debug(1, "Warning: CSI ?K not implemented");
+      else if (prefix || postfix) {
+        debug(1, "Invalid CSI K sequence: " + comstr);
+        return;
+      }
+      /* 0 (default): right, 1: left, 2: all.  Include cursor position. */
+      if (params[0] == 1) {
+        start = 0;
+        end = this.c.x;
+      }
+      else if (params[0] == 2) {
+        start = 0;
+        end = 79;
+      }
+      else {
+        start = this.c.x;
+        end = 79;
+      }
+      var rowdiv = this.screen.childNodes[this.c.y];
+      for (var i = start; i <= end; i++) {
+        rowdiv.replaceChild(this.makeCell(' '), rowdiv.childNodes[i]);
+      }
+      /* Deleting stuff tends to clear this */
+      this.offedge = false;
+      /* The active position is always cleared, so the cursor must be made 
+       * visible again. */
+      this.flipCursor();
+    }
+    else if (c == 76 || c == 77) {
+      /* L - insert lines at the current position.
+       * M - delete current lines */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI sequence: " + comstr);
+        return;
+      }
+      /* CSI LM have no effect outside of the scrolling region */
+      if (this.c.y < this.scrT || this.c.y > this.scrB)
+        return;
+      this.flipCursor();
+      while (count > 0) {
+        var blankrow = this.makeRow();
+        if (c == 76) {
+          this.historize(this.scrB);
+          this.screen.insertBefore(blankrow, this.screen.childNodes[this.c.y]);
+        }
+        else {
+          if (this.scrB == 23)
+            this.screen.appendChild(blankrow);
+          else
+            this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB
+                    + 1]);
+          this.historize(this.c.y);
+        }
+        count--;
+      }
+      /* It doesn't seem necessary to reset this, but xterm does it. */
+      this.offedge = false;
+      this.flipCursor();
+    }
+    else if (c == 80) {
+      /* P - delete at active position, causing cells on the right to shift. */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI P sequence: " + comstr);
+        return;
+      }
+      var cursrow = this.screen.childNodes[this.c.y];
+      while (count > 0) {
+        cursrow.removeChild(cursrow.childNodes[this.c.x]);
+        cursrow.appendChild(this.makeCell(' '));
+        count--;
+      }
+      this.offedge = false;
+      this.flipCursor();
+    }
+    else if (c == 83 || c == 84) {
+      /* S - scroll up, T - scroll down */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI sequence: " + comstr);
+        return;
+      }
+      if (c == 83)
+        this.scroll(count);
+      else
+        this.scroll(-count);
+    }
+    else if (c == 88) {
+      /* X - erase characters */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI sequence: " + comstr);
+        return;
+      }
+      var row = this.screen.childNodes[this.c.y];
+      for (var i = 0; i < count && this.c.x + i < 80; i++) {
+        row.replaceChild(this.makeCell(' '), row.childNodes[this.c.x + i]);
+      }
+      this.flipCursor();
+    }
+    else if (c == 90) {
+      /* Z - tab backwards */
+      var x = this.c.x;
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI Z sequence: " + comstr);
+        return;
+      }
+      while (count > 0) {
+        x = 8 * (Math.ceil(x / 8) - 1);
+        if (x < 0) {
+          x = 0;
+          break;
+        }
+        count--;
+      }
+      this.cmove(null, x);
+    }
+    else if (c == 96) {
+      /* ` - go to col */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI ` sequence: " + comstr);
+        return;
+      }
+      this.cmove(null, count - 1);
+    }
+    else if (c == 100) {
+      /* d - go to row */
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI d sequence: " + comstr);
+        return;
+      }
+      this.cmove(count - 1, null);
+    }
+    else if (c == 102) {
+      /* f - move */
+      var x = 0;
+      var y = 0;
+      if (prefix || postfix) {
+        debug(1, "Invalid CSI f sequence: " + comstr);
+        return;
+      }
+      if (params[0])
+        y = params[0] - 1;
+      if (params[1])
+        x = params[1] - 1;
+      this.cmove(y, x);
+    }
+    else if (c == 104) {
+      /* h - set modes */
+      if (prefix != '?') {
+        debug(1, "Unimplemented CSI sequence: " + comstr);
+        return;
+      }
+      for (var i = 0; i < params.length; i++) {
+        if (params[i] == 1) {
+          keyHexCodes.appCursor(true);
+        }
+        else if (params[i] == 1048) {
+          this.saveCursor();
+        }
+        else if (params[i] == 1049) {
+          this.toAltBuf();
+        }
+        else {
+          debug(1, "Unimplemented CSI ?h parameter: " + params[i]);
+        }
+      }
+    }
+    else if (c == 108) {
+      /* l - reset modes */
+      if (prefix != '?') {
+        debug(1, "Unimplemented CSI sequence: " + comstr);
+        return;
+      }
+      for (var i = 0; i < params.length; i++) {
+        if (params[i] == 1) {
+          keyHexCodes.appCursor(false);
+        }
+        else if (params[i] == 1048) {
+          this.restoreCursor();
+        }
+        else if (params[i] == 1049) {
+          this.toNormBuf();
+        }
+        else {
+          debug(1, "Unimplemented CSI ?l parameter: " + params[i]);
+        }
+      }
+    }
+    else if (c == 109) {
+      /* m - character attributes */
+      if (params.length == 0)
+        this.clearAttrs();
+      for (var i = 0; i < params.length; i++) {
+        if (params[i] == null || params[i] == 0)
+          this.clearAttrs();
+        else if (params[i] == 1)
+          this.c.bold = true;
+        else if (params[i] == 4)
+          this.c.uline = true;
+        else if (params[i] == 7)
+          this.c.inverse = true;
+        else if (params[i] == 22)
+          this.c.bold = false;
+        else if (params[i] == 24)
+          this.c.uline = false;
+        else if (params[i] == 27)
+          this.c.inverse = false;
+        else if (params[i] >= 30 && params[i] <= 37)
+          this.c.fg = params[i] - 30;
+        else if (params[i] == 39)
+          this.c.fg = null;
+        else if (params[i] >= 40 && params[i] <= 47)
+          this.c.bg = params[i] - 40;
+        else if (params[i] == 49)
+          this.c.bg = null;
+        else if (params[i] >= 90 && params[i] <= 97)
+          this.c.fg = params[i] - 82;
+        else if (params[i] >= 100 && params[i] <= 107)
+          this.c.bg = params[i] - 92;
+        else if (params[i] == 38 && params[i + 1] == 5) {
+          if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256)
+            this.c.fg = params[i+2];
+          i += 2;
+        }
+        else if (params[i] == 48 && params[i + 1] == 5) {
+          if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256)
+            this.c.bg = params[i+2];
+          i += 2;
+        }
+        else
+          debug(1, "Unimplemented CSI m parameter: " + params[i]);
+      }
+    }
+    else if (c == 114) {
+      /* r - set scrolling region */
+      var t = 0;
+      var b = 23;
+      if (params[0] && params[0] <= 23)
+        t = params[0] - 1;
+      if (params[1] && params[1] <= 24)
+        b = params[1] - 1;
+      if (b <= t)
+        return;
+      this.scrT = t;
+      this.scrB = b;
+      this.cmove(0, 0);
+    }
+    else {
+      debug(1, "Unimplemented CSI sequence: " + comstr);
+    }
+    return;
+  },
+  oscProcess: function () {
+    var numstr = "";
+    var i;
+    for (i = 1; i < this.comseq.length; i++) {
+      if (this.comseq[i] >= 48 && this.comseq[i] <= 57)
+        numstr += String.fromCharCode(this.comseq[i]);
+      else
+        break;
+    }
+    if (this.comseq[i] != 59) {
+      debug(1, "Invalid OSC sequence");
+      return;
+    }
+    var codenum = Number(numstr);
+    var msgstr = "";
+    i++;
+    while (i < this.comseq.length) {
+      msgstr += String.fromCharCode(this.comseq[i]);
+      i++;
+    }
+    if (codenum == 0 || codenum == 2) {
+      setTitle(msgstr);
+    }
+    else
+      debug(1, "Unimplemented OSC command " + codenum + " " + msgstr);
+    return;
+  },
+  makeCell: function(c) {
+    var tnode;
+    if (c && c.charAt && c.charAt(0))
+      tnode = document.createTextNode(c.charAt(0));
+    else
+      tnode = document.createTextNode(' ');
+    var cell = document.createElement("span");
+    cell.className = "termcell";
+    /* cssColor will handle reverse-video and bold->bright */
+    cell.style.color = this.cssColor(true);
+    cell.style.backgroundColor = this.cssColor(false);
+    if (this.c.bold)
+      cell.style.fontWeight = "bold";
+    if (this.c.uline)
+      cell.style.textDecoration = "underline";
+    cell.appendChild(tnode);
+    return cell;
+  },
+  makeRow: function() {
+    var blankrow = document.createElement("div");
+    blankrow.className = "termrow";
+    for (var i = 0; i < 80; i++)
+      blankrow.appendChild(this.makeCell(' '));
+    return blankrow;
+  }
+};
+
+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 dchunk(codes) {
+  var dstr = "Chunk: ";
+  for (var i = 0; i < codes.length; i++) {
+    if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160))
+      dstr += "\\x" + codes[i].toString(16);
+    else
+      dstr += String.fromCharCode(codes[i]);
+  }
+  debug(1, dstr);
+  return;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tty.css	Sun May 06 08:45:40 2012 -0700
@@ -0,0 +1,90 @@
+div#top {
+	font-size: 1.2em;
+	text-align: center;
+	margin: 0.2em;
+}
+img#bell {
+	visibility: hidden;
+	margin-left: 2em;
+}
+div.keyrow {
+	font-size: 1.2em;
+}
+div.keyrow > div:first-child {
+	clear: left;
+}
+div.key {
+	width: 2em;
+	height: 2em;
+	float: left;
+	border: 2px solid black;
+	margin: 0.2em;
+	text-align: center;
+}
+div.keysel {
+	width: 2em;
+	height: 2em;
+	float: left;
+	border: 2px solid black;
+	margin: 0.2em;
+	text-align: center;
+	background-color: #C0FFC0;
+}
+div#shiftkey {
+	width: 4em;
+}
+div#ctrlkey {
+	width: 3em;
+}
+div#spacebar {
+	width: 8em;
+	margin-left: 12em;
+}
+div#termwrap {
+	margin: 0.5em auto;
+	padding: 1em;
+	background-color: #808080;
+	border: 0.25em solid #202020;
+	border-radius: 1em;
+}
+div#inwrap {
+	overflow-y: scroll;
+}
+div#term {
+	display: table;
+	font-size: 12px;
+	font-family: monospace;
+	white-space: pre;
+}
+div#term > div {
+	display: table-row-group;
+}
+div.termrow {
+	display: table-row;
+}
+span.termcell {
+	display: table-cell;
+}
+div.rbutton {
+	float: right;
+	clear: right;
+	border: 2px solid black;
+	text-align: center;
+	margin: 0.2em;
+	font-size: 1.2em;
+	padding: 0.1em;
+}
+div.rbutton > span {
+	border: 1px solid black;
+	padding: 0 0.1em;
+}
+div#debug {
+	width: 100%;
+	height: 10em;
+	overflow: scroll;
+	white-space: pre;
+	clear: both;
+}
+div#debug > div {
+	font-family: monospace;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webtty.js	Sun May 06 08:45:40 2012 -0700
@@ -0,0 +1,329 @@
+#!/usr/bin/env node
+var http = require('http');
+var url = require('url');
+var path = require('path');
+var fs = require('fs');
+//var tty = require("tty");
+var child_process = require("child_process");
+
+var serveStaticRoot = "/home/elwin/hk/nodejs/rlg/s/";
+var sessions = {};
+
+/* Constructor for TermSessions.  Note that it opens the terminal and 
+ * adds itself to the sessions dict. 
+ */
+function TermSession(sessid) {
+  //var pterm = tty.open("/bin/bash");
+  this.child = child_process.spawn("./ptywreck/ptyhelperC", ["bash"]);
+  var ss = this;
+  /* Eventually we'll need to make sure the sessid isn't in use yet. */
+  this.sessid = sessid;
+  //this.ptmx = pterm[0];
+  //this.child = pterm[1];
+  this.alive = true;
+  this.data = [];
+  this.child.stdout.on("data", function (buf) {
+    ss.data.push(buf);
+  });
+  this.child.stderr.on("data", function (buf) {
+    ss.data.push(buf);
+  });
+  this.child.on("exit", function (code, signal) {
+    ss.exitcode = (code != null ? code : 255);
+    ss.alive = false;
+    /* Wait for all the data to get collected */
+    setTimeout(ss.cleanup, 1000);
+  });
+  this.write = function (data) {
+    if (this.alive)
+      this.child.stdin.write(data);
+    /* Otherwise, throw some kind of exception? */
+  };
+  this.read = function () {
+    if (this.data.length == 0)
+      return null;
+    var pos = 0;
+    var i = 0;
+    for (i = 0; i < this.data.length; i++)
+      pos += this.data[i].length;
+    var nbuf = new Buffer(pos);
+    var tptr;
+    pos = 0;
+    while (this.data.length > 0) {
+      tptr = this.data.shift();
+      tptr.copy(nbuf, pos);
+      pos += tptr.length;
+    }
+    return nbuf;
+  };
+  this.close = function () {
+    if (this.alive)
+      this.child.kill('SIGHUP');
+  };
+  this.cleanup = function () {
+    /* Call this when the child is dead. */
+    if (this.alive)
+      return;
+    //ss.ptmx.destroy();
+    /* Give the client a chance to read any leftover data. */
+    if (ss.data.length > 0)
+      setTimeout(ss.remove, 8000);
+    else
+      ss.remove();
+  };
+  this.remove = function () {
+    delete sessions[ss.sessid];
+    console.log("Session " + this.sessid + " removed.");
+  };
+  sessions[sessid] = this;
+}
+
+function randkey() {
+  rnum = Math.floor(Math.random() * 65536 * 65536);
+  hexstr = rnum.toString(16);
+  while (hexstr.length < 8)
+    hexstr = "0" + hexstr;
+  return hexstr;
+}
+
+/* Returns a list of the cookies in the request, obviously. */
+function getCookies(req) {
+  cookies = [];
+  if ("cookie" in req.headers) {
+    cookstrs = req.headers["cookie"].split("; ");
+    for (var i = 0; i < cookstrs.length; i++) {
+      eqsign = cookstrs[i].indexOf("=");
+      if (eqsign > 0) {
+        name = cookstrs[i].slice(0, eqsign).toLowerCase();
+        val = cookstrs[i].slice(eqsign + 1);
+        cookies[name] = val;
+      }
+      else if (eqsign < 0)
+        cookies[cookstrs[i]] = null;
+    }
+  }
+  return cookies;
+}
+
+function urlDec(encstr) {
+  var decstr = "";
+  var tnum;
+  for (var i = 0; i < encstr.length; i++)
+  {
+    if (encstr.charAt(i) == "+")
+      decstr += " ";
+    else if (encstr.charAt(i) == "%")
+    {
+      tnum = Number("0x" + encstr.slice(i + 1, 2));
+      if (!isNaN(tnum) && tnum >= 0)
+        decstr += String.fromCharCode(tnum);
+      i += 2;
+    }
+    else
+      decstr += encstr.charAt(i);
+  }
+  return decstr;
+}
+
+/* Returns the contents of a form */
+function getFormValues(formtext) {
+  var pairstrs = formtext.split("&");
+  var data = {};
+  for (var i = 0; i < pairstrs.length; i++)
+  {
+    var eqsign = pairstrs[i].indexOf("=");
+    if (eqsign > 0) {
+      rawname = pairstrs[i].slice(0, eqsign);
+      rawval = pairstrs[i].slice(eqsign + 1);
+      name = urlDec(rawname);
+      val = urlDec(rawval);
+      if (!(name in data))
+        data[name] = [];
+      data[name].push(val);
+    }
+  }
+  return data;
+}
+
+function login(req, res, formdata) {
+  var resheaders = {'Content-Type': 'text/plain'};
+  var sessid = randkey();
+  var nsession = new TermSession(sessid);
+  resheaders["Set-Cookie"] = "ID=" + sessid;
+  res.writeHead(200, resheaders);
+  res.write("l1\n" + sessid + "\n");
+  res.end();
+  console.log("Started new session with key " + sessid + ", pid " + nsession.child.pid);
+  return;
+}
+
+function findTermSession(req) {
+  var cookies = getCookies(req);
+  if ("id" in cookies) {
+    var sessid = cookies["id"];
+    if (sessid in sessions) {
+      return sessions[sessid];
+    }
+  }
+  return null;
+}
+
+function serveStatic(req, res, fname) {
+  var nname = path.normalize(fname);
+  if (nname == "" || nname == "/")
+    nname = "index.html";
+  if (nname.match(/\/$/))
+    path.join(nname, "index.html"); /* it was a directory */
+  var realname = path.join(serveStaticRoot, nname);
+  var extension = path.extname(realname);
+  path.exists(realname, function (exists) {
+    var resheaders = {};
+    if (!exists || !extension || extension == ".html")
+      resheaders["Content-Type"] = "text/html";
+    else if (extension == ".png")
+      resheaders["Content-Type"] = "image/png";
+    else if (extension == ".css")
+      resheaders["Content-Type"] = "text/css";
+    else if (extension == ".js")
+      resheaders["Content-Type"] = "text/javascript";
+    else if (extension == ".svg")
+      resheaders["Content-Type"] = "image/svg+xml";
+    else
+      resheaders["Content-Type"] = "application/octet-stream";
+    if (exists) {
+      /* Not nice, not sensible.  First see if it's readable, then respond
+       * 200 or 500.  Don't throw nasty errors. */
+      res.writeHead(200, resheaders);
+      fs.readFile(realname, function (error, data) {
+        if (error) throw error;
+        res.write(data);
+        res.end();
+      });
+    }
+    else {
+      res.writeHead(404, resheaders);
+      res.write("<html><head><title>" + nname + "</title></head>\n<body><h1>" + nname + " Not Found</h1></body></html>\n");
+      res.end();
+    }
+  });
+  return;
+}
+
+function readFeed(res, term) {
+  res.writeHead(200, { "Content-Type": "text/plain" });
+  if (term) {
+    var result = term.read();
+    if (result == null)
+      resultstr = "";
+    else
+      resultstr = result.toString("hex");
+    res.write("d" + resultstr.length.toString() + "\n" + resultstr + "\n");
+  }
+  else {
+    //console.log("Where's the term?");
+    res.write("d0\n\n");
+  }
+}
+
+var errorcodes = [ "Generic Error", "Not logged in", "Invalid data" ];
+
+function sendError(res, ecode) {
+  res.writeHead(200, { "Content-Type": "text/plain" });
+  if (ecode < errorcodes.length && ecode > 0)
+    res.write("E" + ecode + '\n' + errorcodes[ecode] + '\n');
+  else
+    res.write("E0\nGeneric Error\n");
+  res.end();
+}
+
+function handler(req, res) {
+  /* default headers for the response */
+  var resheaders = {'Content-Type': 'text/html'};
+  /* The request body will be added to this as it arrives. */
+  var reqbody = "";
+  var formdata;
+
+  /* Register a listener to get the body. */
+  function moredata(chunk) {
+    reqbody += chunk;
+  }
+  req.on('data', moredata);
+
+  /* This will send the response once the whole request is here. */
+  function respond() {
+    var target = url.parse(req.url).pathname;
+    var cterm = findTermSession(req);
+    /* First figure out if the client is POSTing to a command interface. */
+    if (req.method == 'POST') {
+      formdata = getFormValues(reqbody);
+      if (target == '/feed') {
+        if (!cterm) {
+          sendError(res, 1);
+          return;
+        }
+        if (formdata["quit"] == "quit") {
+          /* The client wants to quit. */
+          // FIXME need to send a message back to the client
+          cterm.close();
+        }
+        else if (formdata["keys"]) {
+          /* process the keys */
+          hexstr = formdata["keys"][0].replace(/[^0-9a-f]/gi, "");
+          if (hexstr.length % 2 != 0) {
+            sendError(res, 2);
+            return;
+          }
+          keybuf = new Buffer(hexstr, "hex");
+          cterm.write(keybuf);
+        }
+        readFeed(res, cterm);
+        res.end();
+      }
+      else if (target == "/login") {
+        login(req, res, formdata);
+      }
+      else {
+        res.writeHead(405, resheaders);
+        res.end();
+      }
+    }
+    else if (req.method == 'GET' || req.method == 'HEAD') {
+      if (target == '/feed') {
+        if (!cterm) {
+          sendError(res, 1);
+          return;
+        }
+        readFeed(res, cterm);
+        res.end();
+      }
+      /* Default page, create a new term */
+      /* FIXME New term not created anymore, is a special case still needed? */
+      else if (target == '/') {
+        serveStatic(req, res, "/");
+      }
+      else /* Go look for it in the filesystem */
+        serveStatic(req, res, target);
+    }
+    else { /* Some other method */
+      res.writeHead(501, resheaders);
+      res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
+      res.end();
+    }
+    return;
+  }
+  req.on('end', respond);
+
+}
+
+process.on("exit", function () {
+  for (var sessid in sessions) {
+    if (sessions[sessid].alive)
+      sessions[sessid].child.kill('SIGHUP');
+  }
+  console.log("Quitting...");
+  return;
+});
+
+process.env["TERM"] = "xterm-256color";
+http.createServer(handler).listen(8080, "127.0.0.1");
+console.log('Server running at http://127.0.0.1:8080/'); 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/webttyd.js	Sun May 06 08:45:40 2012 -0700
@@ -0,0 +1,531 @@
+#!/usr/bin/env node
+
+// If you can't quite trust node to find it on its own
+var localModules = '/usr/local/lib/node_modules/';
+var http = require('http');
+var url = require('url');
+var path = require('path');
+var fs = require('fs');
+//var tty = require("tty");
+var child_process = require('child_process');
+var daemon = require(path.join(localModules, "daemon"));
+
+var chrootDir = "/var/dgl/";
+var dropToUID = 501;
+var dropToGID = 501;
+var serveStaticRoot = "/var/www/"; // inside the chroot
+var passwdfile = "/dgldir/dgl-login";
+var sessions = {};
+
+var games = {
+  "rogue3": {
+    "name": "Rogue V3",
+    "uname": "rogue3",
+    "path": "/bin/rogue3"
+  },
+  "rogue4": {
+    "name": "Rogue V4",
+    "uname": "rogue4",
+    "path": "/bin/rogue4"
+  },
+  "rogue5": {
+    "name": "Rogue V5",
+    "uname": "rogue5",
+    "path": "/bin/rogue5"
+  },
+  "srogue": {
+    "name": "Super-Rogue",
+    "uname": "srogue",
+    "path": "/bin/srogue"
+  }
+};
+
+/* Constructor for TermSessions.  Note that it opens the terminal and 
+ * adds itself to the sessions dict. It currently assumes the user has
+ * been authenticated.
+ */
+function TermSession(game, user, files) {
+  /* First make sure starting the game will work. */
+  if (!(game in games)) {
+    // TODO: throw an exception instead
+    return null;
+  }
+  /* This order seems to best avoid race conditions... */
+  this.alive = false;
+  this.sessid = randkey();
+  while (this.sessid in sessions) {
+    this.sessid = randkey();
+  }
+  /* Grab a spot in the sessions table. */
+  sessions[this.sessid] = this;
+  /* TODO handle tty-opening errors */
+  //var pterm = tty.open(games[game].path, ["-n", user.toString()]);
+  /* TODO make argument-finding into a method */
+  args = [games[game].path, "-n", user.toString()];
+  this.child = child_process.spawn("/bin/ptyhelper", args);
+  var ss = this;
+  //this.ptmx = pterm[0];
+  //this.child = pterm[1];
+  this.alive = true;
+  this.data = [];
+  this.lock = files[0];
+  fs.writeFile(this.lock, this.child.pid.toString() + '\n80\n24\n', "utf8"); 
+  this.record = fs.createWriteStream(files[1], { mode: 0664 });
+  /* END setup */
+  function ttyrec_chunk(buf) {
+    var ts = new Date();
+    var chunk = new Buffer(buf.length + 12);
+    /* TTYREC headers */
+    chunk.writeUInt32LE(Math.floor(ts.getTime() / 1000), 0);
+    chunk.writeUInt32LE(1000 * (ts.getTime() % 1000), 4);
+    chunk.writeUInt32LE(buf.length, 8);
+    buf.copy(chunk, 12);
+    ss.data.push(chunk);
+    ss.record.write(chunk);
+  }
+  this.child.stdout.on("data", ttyrec_chunk);
+  this.child.stderr.on("data", ttyrec_chunk);
+  this.child.on("exit", function (code, signal) {
+    ss.exitcode = (code != null ? code : 255);
+    ss.alive = false;
+    fs.unlink(ss.lock);
+    /* Wait for all the data to get collected */
+    setTimeout(ss.cleanup, 1000);
+  });
+  this.write = function (data) {
+    if (this.alive)
+      this.child.stdin.write(data);
+    /* Otherwise, throw some kind of exception? */
+  };
+  this.read = function () {
+    if (this.data.length == 0)
+      return null;
+    var pos = 0;
+    var i = 0;
+    for (i = 0; i < this.data.length; i++)
+      pos += this.data[i].length - 12;
+    var nbuf = new Buffer(pos);
+    var tptr;
+    pos = 0;
+    while (this.data.length > 0) {
+      tptr = this.data.shift();
+      tptr.copy(nbuf, pos, 12);
+      pos += tptr.length - 12;
+    }
+    return nbuf;
+  };
+  this.close = function () {
+    if (this.alive)
+      this.child.kill('SIGHUP');
+  };
+  this.cleanup = function () {
+    /* Call this when the child is dead. */
+    if (this.alive)
+      return;
+    //ss.ptmx.destroy();
+    ss.record.end();
+    /* Give the client a chance to read any leftover data. */
+    if (ss.data.length > 0)
+      setTimeout(ss.remove, 8000);
+    else
+      ss.remove();
+  };
+  this.remove = function () {
+    delete sessions[ss.sessid];
+    console.log("Session " + this.sessid + " removed.");
+  };
+}
+
+/* A few utility functions */
+function timestamp() {
+  dd = new Date();
+  sd = dd.toISOString();
+  sd = sd.slice(0, sd.indexOf("."));
+  return sd.replace("T", ".");
+}
+
+function randkey() {
+  rnum = Math.floor(Math.random() * 65536 * 65536);
+  hexstr = rnum.toString(16);
+  while (hexstr.length < 8)
+    hexstr = "0" + hexstr;
+  return hexstr;
+}
+
+/* Returns a list of the cookies in the request, obviously. */
+function getCookies(req) {
+  cookies = [];
+  if ("cookie" in req.headers) {
+    cookstrs = req.headers["cookie"].split("; ");
+    for (var i = 0; i < cookstrs.length; i++) {
+      eqsign = cookstrs[i].indexOf("=");
+      if (eqsign > 0) {
+        name = cookstrs[i].slice(0, eqsign).toLowerCase();
+        val = cookstrs[i].slice(eqsign + 1);
+        cookies[name] = val;
+      }
+      else if (eqsign < 0)
+        cookies[cookstrs[i]] = null;
+    }
+  }
+  return cookies;
+}
+
+function urlDec(encstr) {
+  var decstr = "";
+  var tnum;
+  for (var i = 0; i < encstr.length; i++)
+  {
+    if (encstr.charAt(i) == "+")
+      decstr += " ";
+    else if (encstr.charAt(i) == "%")
+    {
+      tnum = Number("0x" + encstr.slice(i + 1, 2));
+      if (!isNaN(tnum) && tnum >= 0)
+        decstr += String.fromCharCode(tnum);
+      i += 2;
+    }
+    else
+      decstr += encstr.charAt(i);
+  }
+  return decstr;
+}
+
+/* Returns the contents of a form */
+function getFormValues(formtext) {
+  var pairstrs = formtext.split("&");
+  var data = {};
+  for (var i = 0; i < pairstrs.length; i++)
+  {
+    var eqsign = pairstrs[i].indexOf("=");
+    if (eqsign > 0) {
+      rawname = pairstrs[i].slice(0, eqsign);
+      rawval = pairstrs[i].slice(eqsign + 1);
+      name = urlDec(rawname);
+      val = urlDec(rawval);
+      if (!(name in data))
+        data[name] = [];
+      data[name].push(val);
+    }
+  }
+  return data;
+}
+
+function auth(username, password) {
+  // Real authentication not implemented
+  return true;
+}
+
+function login(req, res, formdata) {
+  if (!("game" in formdata)) {
+    sendError(res, 2, "No game specified.");
+    return;
+  }
+  else if (!("name" in formdata)) {
+    sendError(res, 2, "Username not given.");
+    return;
+  }
+  else if (!("pw" in formdata)) {
+    sendError(res, 2, "Password not given.");
+    return;
+  }
+  var username = formdata["name"][0];
+  var password = formdata["pw"][0];
+  var gname = formdata["game"][0];
+  if (!(gname in games)) {
+    sendError(res, 2, "No such game: " + gname);
+    console.log("Request for nonexistant game \"" + gname + "\"");
+    return;
+  }
+  var progressdir = "/dgldir/inprogress-" + games[gname].uname;
+
+  // This sets up the game once starting is approved.
+  function startgame() {
+    var ts = timestamp();
+    var lockfile = path.join(progressdir, username + ":node:" + ts + ".ttyrec");
+    var ttyrec = path.join("/dgldir/ttyrec", username, gname, ts + ".ttyrec");
+    var nsession = new TermSession(gname, username, [lockfile, ttyrec]);
+    if (nsession) {
+      /* Technically there's a race condition for the "lock"file, but since 
+       * it requires the user deliberately starting two games at similar times, 
+       * it's not too serious. We can't get O_EXCL in Node anyway. */
+      res.writeHead(200, {'Content-Type': 'text/plain'});
+      res.write("l1\n" + nsession.sessid + "\n");
+      res.end();
+      console.log("%s playing %s (key %s, pid %d)", username, gname, 
+                  nsession.sessid, nsession.child.pid);
+    }
+    else {
+      sendError(res, 5, "Failed to open TTY");
+      console.log("Unable to allocate TTY for " + gname);
+    }
+  }
+  function checkit(code, signal) {
+    // check the password
+    if (code != 0) {
+      sendError(res, 3);
+      console.log("Password check failed for user " + username);
+      return;
+    }
+    // check for an existing game
+    fs.readdir(progressdir, function(err, files) {
+      if (!err) {
+        var fre = RegExp("^" + username + ":");
+        for (var i = 0; i < files.length; i++) {
+          if (files[i].match(fre)) {
+            sendError(res, 4, null);
+            return;
+          }
+        }
+      }
+      // If progressdir isn't readable, start a new game anyway.
+      startgame();
+    });
+  }
+  /* Look for the user in the password file */
+  fs.readFile(passwdfile, "utf8", function(err, data) {
+    if (err) {
+      sendError(res, 3);
+      console.log("Can't authenticate: " + err.toString());
+      return;
+    }
+    var dlines = data.split('\n');
+    for (var n = 0; n < dlines.length; n++) {
+      var fields = dlines[n].split(':');
+      if (fields[0] == username) {
+        // check the password with the quickrypt utility
+        checker = require('child_process').spawn("/bin/quickrypt")
+        checker.on("exit", checkit);
+        checker.stdin.end(password + '\n' + fields[2] + '\n', "utf8");
+        return;
+      }
+    }
+    sendError(res, 3);
+    console.log("Attempted login by nonexistent user " + username);
+  });
+  return;
+}
+
+function logout(term, res) {
+  if (!term.alive) {
+    sendError(res, 1, null);
+    return;
+  }
+  cterm.close();
+  var resheaders = {'Content-Type': 'text/plain'};
+  res.writeHead(200, resheaders);
+  res.write("q1\n\n");
+  res.end();
+  return;
+}
+
+function findTermSession(formdata) {
+  if ("id" in formdata) {
+    var sessid = formdata["id"][0];
+    if (sessid in sessions) {
+      return sessions[sessid];
+    }
+  }
+  return null;
+}
+
+function serveStatic(req, res, fname) {
+  var nname = path.normalize(fname);
+  if (nname == "" || nname == "/")
+    nname = "index.html";
+  if (nname.match(/\/$/))
+    path.join(nname, "index.html"); /* it was a directory */
+  var realname = path.join(serveStaticRoot, nname);
+  var extension = path.extname(realname);
+  path.exists(realname, function (exists) {
+    var resheaders = {};
+    if (!exists || !extension || extension == ".html")
+      resheaders["Content-Type"] = "text/html";
+    else if (extension == ".png")
+      resheaders["Content-Type"] = "image/png";
+    else if (extension == ".css")
+      resheaders["Content-Type"] = "text/css";
+    else if (extension == ".js")
+      resheaders["Content-Type"] = "text/javascript";
+    else if (extension == ".svg")
+      resheaders["Content-Type"] = "image/svg+xml";
+    else
+      resheaders["Content-Type"] = "application/octet-stream";
+    if (exists) {
+      fs.readFile(realname, function (error, data) {
+        if (error) {
+          res.writeHead(500, {});
+          res.end();
+        }
+        else {
+          res.writeHead(200, resheaders);
+          res.write(data);
+          res.end();
+        }
+      });
+    }
+    else {
+      res.writeHead(404, resheaders);
+      res.write("<html><head><title>" + nname + "</title></head>\n<body><h1>" + nname + " Not Found</h1></body></html>\n");
+      res.end();
+    }
+  });
+  return;
+}
+
+function readFeed(res, term) {
+  if (term) {
+    var result = term.read();
+    res.writeHead(200, { "Content-Type": "text/plain" });
+    if (result == null)
+      resultstr = "";
+    else
+      resultstr = result.toString("hex");
+    if (result == null && !term.alive) {
+      /* Child has terminated and data is flushed. */
+      res.write("q1\n\n");
+    }
+    else
+      res.write("d" + resultstr.length.toString() + "\n" + resultstr + "\n");
+    res.end();
+  }
+  else {
+    //console.log("Where's the term?");
+    sendError(res, 1, null);
+  }
+}
+
+var errorcodes = [ "Generic Error", "Not logged in", "Invalid data", 
+        "Login failed", "Already playing", "Game launch failed" ];
+
+function sendError(res, ecode, msg) {
+  res.writeHead(200, { "Content-Type": "text/plain" });
+  if (ecode < errorcodes.length && ecode > 0) {
+    var emsg = errorcodes[ecode];
+    if (msg)
+      emsg += ": " + msg;
+    res.write("E" + ecode + '\n' + emsg + '\n');
+  }
+  else
+    res.write("E0\nGeneric Error\n");
+  res.end();
+}
+
+function handler(req, res) {
+  /* default headers for the response */
+  var resheaders = {'Content-Type': 'text/html'};
+  /* The request body will be added to this as it arrives. */
+  var reqbody = "";
+  var formdata;
+
+  /* Register a listener to get the body. */
+  function moredata(chunk) {
+    reqbody += chunk;
+  }
+  req.on('data', moredata);
+
+  /* This will send the response once the whole request is here. */
+  function respond() {
+    formdata = getFormValues(reqbody);
+    var target = url.parse(req.url).pathname;
+    var cterm = findTermSession(formdata);
+    /* First figure out if the client is POSTing to a command interface. */
+    if (req.method == 'POST') {
+      if (target == '/feed') {
+        if (!cterm) {
+          sendError(res, 1, null);
+          return;
+        }
+        if ("quit" in formdata) {
+          /* The client wants to terminate the process. */
+          logout(cterm, res);
+        }
+        else if (formdata["keys"]) {
+          /* process the keys */
+          hexstr = formdata["keys"][0].replace(/[^0-9a-f]/gi, "");
+          if (hexstr.length % 2 != 0) {
+            sendError(res, 2, "incomplete byte");
+            return;
+          }
+          keybuf = new Buffer(hexstr, "hex");
+          cterm.write(keybuf);
+        }
+        readFeed(res, cterm);
+      }
+      else if (target == "/login") {
+        login(req, res, formdata);
+      }
+      else {
+        res.writeHead(405, resheaders);
+        res.end();
+      }
+    }
+    else if (req.method == 'GET' || req.method == 'HEAD') {
+      if (target == '/feed') {
+        if (!cterm) {
+          sendError(res, 1, null);
+          return;
+        }
+        readFeed(res, cterm);
+      }
+      /* Default page, create a new term */
+      /* FIXME New term not created anymore, is a special case still needed? */
+      else if (target == '/') {
+        serveStatic(req, res, "/");
+      }
+      else /* Go look for it in the filesystem */
+        serveStatic(req, res, target);
+    }
+    else { /* Some other method */
+      res.writeHead(501, resheaders);
+      res.write("<html><head><title>501</title></head>\n<body><h1>501 Not Implemented</h1></body></html>\n");
+      res.end();
+    }
+    return;
+  }
+  req.on('end', respond);
+
+}
+
+process.on("exit", function () {
+  for (var sessid in sessions) {
+    if (sessions[sessid].alive)
+      sessions[sessid].child.kill('SIGHUP');
+  }
+  console.log("Quitting...");
+  return;
+});
+
+/* Initialization STARTS HERE */
+process.env["TERM"] = "xterm-256color";
+
+if (process.getuid() != 0) {
+  console.log("Not running as root, cannot chroot.");
+  process.exit(1);
+}
+try {
+  process.chdir(chrootDir); 
+}
+catch (err) {
+  console.log("Cannot enter " + chrootDir + " : " + err);
+  process.exit(1);
+}
+try {
+  daemon.chroot(chrootDir);
+}
+catch (err) {
+  console.log("chroot to " + chrootDir + " failed: " + err);
+  process.exit(1);
+}
+try {
+  // drop gid first, that requires UID=0
+  process.setgid(dropToGID);
+  process.setuid(dropToUID);
+}
+catch (err) {
+  console.log("Could not drop permissions: " + err);
+  process.exit(1);
+}
+
+http.createServer(handler).listen(8080, "127.0.0.1");
+console.log('webttyd running at http://127.0.0.1:8080/');