telnet daemon working
[nikiroo-utils.git] / src / jexer / io / ECMA48Terminal.java
index 7542f8a28bd73fdd686217223eff9dc061378a17..1aafa3c41619a4864d0297f1e966818e39e939da 100644 (file)
@@ -1,16 +1,11 @@
 /**
  * Jexer - Java Text User Interface
  *
- * Version: $Id$
- *
- * Author: Kevin Lamonte, <a href="mailto:kevin.lamonte@gmail.com">kevin.lamonte@gmail.com</a>
- *
  * License: LGPLv3 or later
  *
- * Copyright: This module is licensed under the GNU Lesser General
- * Public License Version 3.  Please see the file "COPYING" in this
- * directory for more information about the GNU Lesser General Public
- * License Version 3.
+ * This module is licensed under the GNU Lesser General Public License
+ * Version 3.  Please see the file "COPYING" in this directory for more
+ * information about the GNU Lesser General Public License Version 3.
  *
  *     Copyright (C) 2015  Kevin Lamonte
  *
  * http://www.gnu.org/licenses/, or write to the Free Software
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
  * 02110-1301 USA
+ *
+ * @author Kevin Lamonte [kevin.lamonte@gmail.com]
+ * @version 1
  */
 package jexer.io;
 
 import java.io.BufferedReader;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.IOException;
@@ -46,7 +46,6 @@ import java.util.Date;
 import java.util.List;
 import java.util.LinkedList;
 
-import jexer.TKeypress;
 import jexer.bits.Color;
 import jexer.event.TInputEvent;
 import jexer.event.TKeypressEvent;
@@ -58,53 +57,77 @@ import jexer.session.TTYSessionInfo;
 import static jexer.TKeypress.*;
 
 /**
- * This class has convenience methods for emitting output to ANSI
- * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys,
- * etc.
+ * This class reads keystrokes and mouse events and emits output to ANSI
+ * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
  */
-public class ECMA48Terminal {
+public final class ECMA48Terminal implements Runnable {
 
     /**
-     * The session information
+     * The session information.
      */
-    public SessionInfo session;
+    private SessionInfo sessionInfo;
 
     /**
-     * Parameters being collected.  E.g. if the string is \033[1;3m, then
-     * params[0] will be 1 and params[1] will be 3.
+     * Getter for sessionInfo.
+     *
+     * @return the SessionInfo
      */
-    private ArrayList<String> params;
+    public SessionInfo getSessionInfo() {
+        return sessionInfo;
+    }
+
+    /**
+     * The event queue, filled up by a thread reading on input.
+     */
+    private List<TInputEvent> eventQueue;
 
     /**
-     * params[paramI] is being appended to.
+     * If true, we want the reader thread to exit gracefully.
      */
-    private int paramI;
+    private boolean stopReaderThread;
 
     /**
-     * States in the input parser
+     * The reader thread.
+     */
+    private Thread readerThread;
+
+    /**
+     * Parameters being collected.  E.g. if the string is \033[1;3m, then
+     * params[0] will be 1 and params[1] will be 3.
+     */
+    private ArrayList<String> params;
+
+    /**
+     * States in the input parser.
      */
     private enum ParseState {
-       GROUND,
-       ESCAPE,
-       ESCAPE_INTERMEDIATE,
-       CSI_ENTRY,
-       CSI_PARAM,
-       // CSI_INTERMEDIATE,
-       MOUSE
+        GROUND,
+        ESCAPE,
+        ESCAPE_INTERMEDIATE,
+        CSI_ENTRY,
+        CSI_PARAM,
+        // CSI_INTERMEDIATE,
+        MOUSE,
+        MOUSE_SGR,
     }
 
     /**
-     * Current parsing state
+     * Current parsing state.
      */
     private ParseState state;
 
     /**
-     * The time we entered ESCAPE.  If we get a bare escape
-     * without a code following it, this is used to return that bare
-     * escape.
+     * The time we entered ESCAPE.  If we get a bare escape without a code
+     * following it, this is used to return that bare escape.
      */
     private long escapeTime;
 
+    /**
+     * The time we last checked the window size.  We try not to spawn stty
+     * more than once per second.
+     */
+    private long windowSizeTime;
+
     /**
      * true if mouse1 was down.  Used to report mouse1 on the release event.
      */
@@ -139,11 +162,19 @@ public class ECMA48Terminal {
 
     /**
      * The terminal's input.  If an InputStream is not specified in the
-     * constructor, then this InputReader will be bound to System.in with
-     * UTF-8 encoding.
+     * constructor, then this InputStreamReader will be bound to System.in
+     * with UTF-8 encoding.
      */
     private Reader input;
 
+    /**
+     * The terminal's raw InputStream.  If an InputStream is not specified in
+     * the constructor, then this InputReader will be bound to System.in.
+     * This is used by run() to see if bytes are available() before calling
+     * (Reader)input.read().
+     */
+    private InputStream inputStream;
+
     /**
      * The terminal's output.  If an OutputStream is not specified in the
      * constructor, then this PrintWriter will be bound to System.out with
@@ -152,12 +183,9 @@ public class ECMA48Terminal {
     private PrintWriter output;
 
     /**
-     * When true, the terminal is sending non-UTF8 bytes when reporting mouse
-     * events.
-     *
-     * TODO: Add broken mouse detection back into the reader.
+     * The listening object that run() wakes up on new input.
      */
-    private boolean brokenTerminalUTFMouse = false;
+    private Object listener;
 
     /**
      * Get the output writer.
@@ -165,21 +193,38 @@ public class ECMA48Terminal {
      * @return the Writer
      */
     public PrintWriter getOutput() {
-       return output;
+        return output;
     }
 
     /**
-     * Call 'stty cooked' to set cooked mode.
+     * Check if there are events in the queue.
+     *
+     * @return if true, getEvents() has something to return to the backend
+     */
+    public boolean hasEvents() {
+        synchronized (eventQueue) {
+            return (eventQueue.size() > 0);
+        }
+    }
+
+    /**
+     * Call 'stty' to set cooked mode.
+     *
+     * <p>Actually executes '/bin/sh -c stty sane cooked &lt; /dev/tty'
      */
     private void sttyCooked() {
-       doStty(false);
+        doStty(false);
     }
 
     /**
-     * Call 'stty raw' to set raw mode.
+     * Call 'stty' to set raw mode.
+     *
+     * <p>Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip
+     * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten
+     * -parenb cs8 min 1 &lt; /dev/tty'
      */
     private void sttyRaw() {
-       doStty(true);
+        doStty(true);
     }
 
     /**
@@ -187,52 +232,52 @@ public class ECMA48Terminal {
      *
      * @param mode if true, set raw mode, otherwise set cooked mode
      */
-    private void doStty(boolean mode) {
-       String [] cmdRaw = {
-           "/bin/sh", "-c", "stty raw < /dev/tty"
-       };
-       String [] cmdCooked = {
-           "/bin/sh", "-c", "stty cooked < /dev/tty"
-       };
-       try {
-           System.out.println("spawn stty");
-
-           Process process;
-           if (mode == true) {
-               process = Runtime.getRuntime().exec(cmdRaw);
-           } else {
-               process = Runtime.getRuntime().exec(cmdCooked);
-           }
-           BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
-           String line = in.readLine();
-           if ((line != null) && (line.length() > 0)) {
-               System.err.println("WEIRD?! Normal output from stty: " + line);
-           }
-           while (true) {
-               BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
-               line = err.readLine();
-               if ((line != null) && (line.length() > 0)) {
-                   System.err.println("Error output from stty: " + line);
-               }
-               try{
-                   process.waitFor();
-                   break;
-               } catch (InterruptedException e) {
-                   e.printStackTrace();
-               }
-           }
-           int rc = process.exitValue();
-           if (rc != 0) {
-               System.err.println("stty returned error code: " + rc);
-           }
-       } catch (IOException e) {
-           e.printStackTrace();
-       }
+    private void doStty(final boolean mode) {
+        String [] cmdRaw = {
+            "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty"
+        };
+        String [] cmdCooked = {
+            "/bin/sh", "-c", "stty sane cooked < /dev/tty"
+        };
+        try {
+            Process process;
+            if (mode) {
+                process = Runtime.getRuntime().exec(cmdRaw);
+            } else {
+                process = Runtime.getRuntime().exec(cmdCooked);
+            }
+            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
+            String line = in.readLine();
+            if ((line != null) && (line.length() > 0)) {
+                System.err.println("WEIRD?! Normal output from stty: " + line);
+            }
+            while (true) {
+                BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8"));
+                line = err.readLine();
+                if ((line != null) && (line.length() > 0)) {
+                    System.err.println("Error output from stty: " + line);
+                }
+                try {
+                    process.waitFor();
+                    break;
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+            int rc = process.exitValue();
+            if (rc != 0) {
+                System.err.println("stty returned error code: " + rc);
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
     }
 
     /**
-     * Constructor sets up state for getEvent()
+     * Constructor sets up state for getEvent().
      *
+     * @param listener the object this backend needs to wake up when new
+     * input comes in
      * @param input an InputStream connected to the remote user, or null for
      * System.in.  If System.in is used, then on non-Windows systems it will
      * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
@@ -240,116 +285,156 @@ public class ECMA48Terminal {
      * @param output an OutputStream connected to the remote user, or null
      * for System.out.  output is always converted to a Writer with UTF-8
      * encoding.
+     * @throws UnsupportedEncodingException if an exception is thrown when
+     * creating the InputStreamReader
      */
-    public ECMA48Terminal(InputStream input, OutputStream output) throws UnsupportedEncodingException {
-
-       reset();
-       mouse1 = false;
-       mouse2 = false;
-       mouse3 = false;
-
-       if (input == null) {
-           this.input = new InputStreamReader(System.in, "UTF-8");
-           sttyRaw();
-           setRawMode = true;
-       } else {
-           this.input = new InputStreamReader(input);
-       }
-       // TODO: include TelnetSocket from NIB and have it implement
-       // SessionInfo
-       if (input instanceof SessionInfo) {
-           session = (SessionInfo)input;
-       }
-       if (session == null) {
-           if (input == null) {
-               // Reading right off the tty
-               session = new TTYSessionInfo();
-           } else {
-               session = new TSessionInfo();
-           }
-       }
-
-       if (output == null) {
-           this.output = new PrintWriter(new OutputStreamWriter(System.out,
-                   "UTF-8"));
-       } else {
-           this.output = new PrintWriter(new OutputStreamWriter(output,
-                   "UTF-8"));
-       }
-
-       // Enable mouse reporting and metaSendsEscape
-       this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
-
-       // Hang onto the window size
-       windowResize = new TResizeEvent(TResizeEvent.Type.Screen,
-           session.getWindowWidth(), session.getWindowHeight());
+    public ECMA48Terminal(final Object listener, final InputStream input,
+        final OutputStream output) throws UnsupportedEncodingException {
+
+        reset();
+        mouse1           = false;
+        mouse2           = false;
+        mouse3           = false;
+        stopReaderThread = false;
+        this.listener    = listener;
+
+        if (input == null) {
+            // inputStream = System.in;
+            inputStream = new FileInputStream(FileDescriptor.in);
+            sttyRaw();
+            setRawMode = true;
+        } else {
+            inputStream = input;
+        }
+        this.input = new InputStreamReader(inputStream, "UTF-8");
+
+        if (input instanceof SessionInfo) {
+            // This is a TelnetInputStream that exposes window size and
+            // environment variables from the telnet layer.
+            sessionInfo = (SessionInfo) input;
+        }
+        if (sessionInfo == null) {
+            if (input == null) {
+                // Reading right off the tty
+                sessionInfo = new TTYSessionInfo();
+            } else {
+                sessionInfo = new TSessionInfo();
+            }
+        }
+
+        if (output == null) {
+            this.output = new PrintWriter(new OutputStreamWriter(System.out,
+                    "UTF-8"));
+        } else {
+            this.output = new PrintWriter(new OutputStreamWriter(output,
+                    "UTF-8"));
+        }
+
+        // Enable mouse reporting and metaSendsEscape
+        this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
+        this.output.flush();
+
+        // Hang onto the window size
+        windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
+            sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
+
+        // Spin up the input reader
+        eventQueue = new LinkedList<TInputEvent>();
+        readerThread = new Thread(this);
+        readerThread.start();
     }
 
     /**
-     * Restore terminal to normal state
+     * Restore terminal to normal state.
      */
     public void shutdown() {
-       if (setRawMode) {
-           sttyCooked();
-           setRawMode = false;
-       }
-       // Disable mouse reporting and show cursor
-       output.printf("%s%s%s", mouse(false), cursor(true), normal());
+
+        // System.err.println("=== shutdown() ==="); System.err.flush();
+
+        // Tell the reader thread to stop looking at input
+        stopReaderThread = true;
+        try {
+            readerThread.join();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        // Disable mouse reporting and show cursor
+        output.printf("%s%s%s", mouse(false), cursor(true), normal());
+        output.flush();
+
+        if (setRawMode) {
+            sttyCooked();
+            setRawMode = false;
+            // We don't close System.in/out
+        } else {
+            // Shut down the streams, this should wake up the reader thread
+            // and make it exit.
+            try {
+                if (input != null) {
+                    input.close();
+                    input = null;
+                }
+                if (output != null) {
+                    output.close();
+                    output = null;
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
     }
 
     /**
-     * Flush output
+     * Flush output.
      */
     public void flush() {
-       output.flush();
+        output.flush();
     }
 
     /**
-     * Reset keyboard/mouse input parser
+     * Reset keyboard/mouse input parser.
      */
     private void reset() {
-       state = ParseState.GROUND;
-       paramI = 0;
-       params.clear();
-       params.add("");
+        state = ParseState.GROUND;
+        params = new ArrayList<String>();
+        params.clear();
+        params.add("");
     }
 
     /**
      * Produce a control character or one of the special ones (ENTER, TAB,
-     * etc.)
+     * etc.).
      *
      * @param ch Unicode code point
-     * @return one KEYPRESS event, either a control character (e.g. isKey ==
+     * @param alt if true, set alt on the TKeypress
+     * @return one TKeypress event, either a control character (e.g. isKey ==
      * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true,
      * fnKey == ESC)
      */
-    private TKeypressEvent controlChar(char ch) {
-       TKeypressEvent event = new TKeypressEvent();
-
-       // System.err.printf("controlChar: %02x\n", ch);
-
-       switch (ch) {
-       case '\r':
-           // ENTER
-           event.key = kbEnter;
-           break;
-       case 0x1B:
-           // ESC
-           event.key = kbEsc;
-           break;
-       case '\t':
-           // TAB
-           event.key = kbTab;
-           break;
-       default:
-           // Make all other control characters come back as the alphabetic
-           // character with the ctrl field set.  So SOH would be 'A' +
-           // ctrl.
-           event.key = new TKeypress(false, 0, (char)(ch + 0x40),
-               false, true, false);
-           break;
-       }
-       return event;
+    private TKeypressEvent controlChar(final char ch, final boolean alt) {
+        // System.err.printf("controlChar: %02x\n", ch);
+
+        switch (ch) {
+        case 0x0D:
+            // Carriage return --> ENTER
+            return new TKeypressEvent(kbEnter, alt, false, false);
+        case 0x0A:
+            // Linefeed --> ENTER
+            return new TKeypressEvent(kbEnter, alt, false, false);
+        case 0x1B:
+            // ESC
+            return new TKeypressEvent(kbEsc, alt, false, false);
+        case '\t':
+            // TAB
+            return new TKeypressEvent(kbTab, alt, false, false);
+        default:
+            // Make all other control characters come back as the alphabetic
+            // character with the ctrl field set.  So SOH would be 'A' +
+            // ctrl.
+            return new TKeypressEvent(false, 0, (char)(ch + 0x40),
+                alt, true, false);
+        }
     }
 
     /**
@@ -358,228 +443,72 @@ public class ECMA48Terminal {
      * @return one KEYPRESS event representing a special key
      */
     private TInputEvent csiFnKey() {
-       int key = 0;
-       int modifier = 0;
-       if (params.size() > 0) {
-           key = Integer.parseInt(params.get(0));
-       }
-       if (params.size() > 1) {
-           modifier = Integer.parseInt(params.get(1));
-       }
-       TKeypressEvent event = new TKeypressEvent();
-
-       switch (modifier) {
-       case 0:
-           // No modifier
-           switch (key) {
-           case 1:
-               event.key = kbHome;
-               break;
-           case 2:
-               event.key = kbIns;
-               break;
-           case 3:
-               event.key = kbDel;
-               break;
-           case 4:
-               event.key = kbEnd;
-               break;
-           case 5:
-               event.key = kbPgUp;
-               break;
-           case 6:
-               event.key = kbPgDn;
-               break;
-           case 15:
-               event.key = kbF5;
-               break;
-           case 17:
-               event.key = kbF6;
-               break;
-           case 18:
-               event.key = kbF7;
-               break;
-           case 19:
-               event.key = kbF8;
-               break;
-           case 20:
-               event.key = kbF9;
-               break;
-           case 21:
-               event.key = kbF10;
-               break;
-           case 23:
-               event.key = kbF11;
-               break;
-           case 24:
-               event.key = kbF12;
-               break;
-           default:
-               // Unknown
-               return null;
-           }
-
-           break;
-       case 2:
-           // Shift
-           switch (key) {
-           case 1:
-               event.key = kbShiftHome;
-               break;
-           case 2:
-               event.key = kbShiftIns;
-               break;
-           case 3:
-               event.key = kbShiftDel;
-               break;
-           case 4:
-               event.key = kbShiftEnd;
-               break;
-           case 5:
-               event.key = kbShiftPgUp;
-               break;
-           case 6:
-               event.key = kbShiftPgDn;
-               break;
-           case 15:
-               event.key = kbShiftF5;
-               break;
-           case 17:
-               event.key = kbShiftF6;
-               break;
-           case 18:
-               event.key = kbShiftF7;
-               break;
-           case 19:
-               event.key = kbShiftF8;
-               break;
-           case 20:
-               event.key = kbShiftF9;
-               break;
-           case 21:
-               event.key = kbShiftF10;
-               break;
-           case 23:
-               event.key = kbShiftF11;
-               break;
-           case 24:
-               event.key = kbShiftF12;
-               break;
-           default:
-               // Unknown
-               return null;
-           }
-           break;
-
-       case 3:
-           // Alt
-           switch (key) {
-           case 1:
-               event.key = kbAltHome;
-               break;
-           case 2:
-               event.key = kbAltIns;
-               break;
-           case 3:
-               event.key = kbAltDel;
-               break;
-           case 4:
-               event.key = kbAltEnd;
-               break;
-           case 5:
-               event.key = kbAltPgUp;
-               break;
-           case 6:
-               event.key = kbAltPgDn;
-               break;
-           case 15:
-               event.key = kbAltF5;
-               break;
-           case 17:
-               event.key = kbAltF6;
-               break;
-           case 18:
-               event.key = kbAltF7;
-               break;
-           case 19:
-               event.key = kbAltF8;
-               break;
-           case 20:
-               event.key = kbAltF9;
-               break;
-           case 21:
-               event.key = kbAltF10;
-               break;
-           case 23:
-               event.key = kbAltF11;
-               break;
-           case 24:
-               event.key = kbAltF12;
-               break;
-           default:
-               // Unknown
-               return null;
-           }
-           break;
-
-       case 5:
-           // Ctrl
-           switch (key) {
-           case 1:
-               event.key = kbCtrlHome;
-               break;
-           case 2:
-               event.key = kbCtrlIns;
-               break;
-           case 3:
-               event.key = kbCtrlDel;
-               break;
-           case 4:
-               event.key = kbCtrlEnd;
-               break;
-           case 5:
-               event.key = kbCtrlPgUp;
-               break;
-           case 6:
-               event.key = kbCtrlPgDn;
-               break;
-           case 15:
-               event.key = kbCtrlF5;
-               break;
-           case 17:
-               event.key = kbCtrlF6;
-               break;
-           case 18:
-               event.key = kbCtrlF7;
-               break;
-           case 19:
-               event.key = kbCtrlF8;
-               break;
-           case 20:
-               event.key = kbCtrlF9;
-               break;
-           case 21:
-               event.key = kbCtrlF10;
-               break;
-           case 23:
-               event.key = kbCtrlF11;
-               break;
-           case 24:
-               event.key = kbCtrlF12;
-               break;
-           default:
-               // Unknown
-               return null;
-           }
-           break;
-
-       default:
-           // Unknown
-           return null;
-       }
-
-       // All OK, return a keypress
-       return event;
+        int key = 0;
+        int modifier = 0;
+        if (params.size() > 0) {
+            key = Integer.parseInt(params.get(0));
+        }
+        if (params.size() > 1) {
+            modifier = Integer.parseInt(params.get(1));
+        }
+        boolean alt = false;
+        boolean ctrl = false;
+        boolean shift = false;
+
+        switch (modifier) {
+        case 0:
+            // No modifier
+            break;
+        case 2:
+            // Shift
+            shift = true;
+            break;
+        case 3:
+            // Alt
+            alt = true;
+            break;
+        case 5:
+            // Ctrl
+            ctrl = true;
+            break;
+        default:
+            // Unknown modifier, bail out
+            return null;
+        }
+
+        switch (key) {
+        case 1:
+            return new TKeypressEvent(kbHome, alt, ctrl, shift);
+        case 2:
+            return new TKeypressEvent(kbIns, alt, ctrl, shift);
+        case 3:
+            return new TKeypressEvent(kbDel, alt, ctrl, shift);
+        case 4:
+            return new TKeypressEvent(kbEnd, alt, ctrl, shift);
+        case 5:
+            return new TKeypressEvent(kbPgUp, alt, ctrl, shift);
+        case 6:
+            return new TKeypressEvent(kbPgDn, alt, ctrl, shift);
+        case 15:
+            return new TKeypressEvent(kbF5, alt, ctrl, shift);
+        case 17:
+            return new TKeypressEvent(kbF6, alt, ctrl, shift);
+        case 18:
+            return new TKeypressEvent(kbF7, alt, ctrl, shift);
+        case 19:
+            return new TKeypressEvent(kbF8, alt, ctrl, shift);
+        case 20:
+            return new TKeypressEvent(kbF9, alt, ctrl, shift);
+        case 21:
+            return new TKeypressEvent(kbF10, alt, ctrl, shift);
+        case 23:
+            return new TKeypressEvent(kbF11, alt, ctrl, shift);
+        case 24:
+            return new TKeypressEvent(kbF12, alt, ctrl, shift);
+        default:
+            // Unknown
+            return null;
+        }
     }
 
     /**
@@ -590,518 +519,635 @@ public class ECMA48Terminal {
      * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
      */
     private TInputEvent parseMouse() {
-       int buttons = params.get(0).charAt(0) - 32;
-       int x = params.get(0).charAt(1) - 32 - 1;
-       int y = params.get(0).charAt(2) - 32 - 1;
-
-       // Clamp X and Y to the physical screen coordinates.
-       if (x >= windowResize.width) {
-           x = windowResize.width - 1;
-       }
-       if (y >= windowResize.height) {
-           y = windowResize.height - 1;
-       }
-
-       TMouseEvent event = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN);
-       event.x = x;
-       event.y = y;
-       event.absoluteX = x;
-       event.absoluteY = y;
-
-       // System.err.printf("buttons: %04x\r\n", buttons);
-
-       switch (buttons) {
-       case 0:
-           event.mouse1 = true;
-           mouse1 = true;
-           break;
-       case 1:
-           event.mouse2 = true;
-           mouse2 = true;
-           break;
-       case 2:
-           event.mouse3 = true;
-           mouse3 = true;
-           break;
-       case 3:
-           // Release or Move
-           if (!mouse1 && !mouse2 && !mouse3) {
-               event.type = TMouseEvent.Type.MOUSE_MOTION;
-           } else {
-               event.type = TMouseEvent.Type.MOUSE_UP;
-           }
-           if (mouse1) {
-               mouse1 = false;
-               event.mouse1 = true;
-           }
-           if (mouse2) {
-               mouse2 = false;
-               event.mouse2 = true;
-           }
-           if (mouse3) {
-               mouse3 = false;
-               event.mouse3 = true;
-           }
-           break;
-
-       case 32:
-           // Dragging with mouse1 down
-           event.mouse1 = true;
-           mouse1 = true;
-           event.type = TMouseEvent.Type.MOUSE_MOTION;
-           break;
-
-       case 33:
-           // Dragging with mouse2 down
-           event.mouse2 = true;
-           mouse2 = true;
-           event.type = TMouseEvent.Type.MOUSE_MOTION;
-           break;
-
-       case 34:
-           // Dragging with mouse3 down
-           event.mouse3 = true;
-           mouse3 = true;
-           event.type = TMouseEvent.Type.MOUSE_MOTION;
-           break;
-
-       case 96:
-           // Dragging with mouse2 down after wheelUp
-           event.mouse2 = true;
-           mouse2 = true;
-           event.type = TMouseEvent.Type.MOUSE_MOTION;
-           break;
-
-       case 97:
-           // Dragging with mouse2 down after wheelDown
-           event.mouse2 = true;
-           mouse2 = true;
-           event.type = TMouseEvent.Type.MOUSE_MOTION;
-           break;
-
-       case 64:
-           event.mouseWheelUp = true;
-           break;
-
-       case 65:
-           event.mouseWheelDown = true;
-           break;
-
-       default:
-           // Unknown, just make it motion
-           event.type = TMouseEvent.Type.MOUSE_MOTION;
-           break;
-       }
-       return event;
+        int buttons = params.get(0).charAt(0) - 32;
+        int x = params.get(0).charAt(1) - 32 - 1;
+        int y = params.get(0).charAt(2) - 32 - 1;
+
+        // Clamp X and Y to the physical screen coordinates.
+        if (x >= windowResize.getWidth()) {
+            x = windowResize.getWidth() - 1;
+        }
+        if (y >= windowResize.getHeight()) {
+            y = windowResize.getHeight() - 1;
+        }
+
+        TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
+        boolean eventMouse1 = false;
+        boolean eventMouse2 = false;
+        boolean eventMouse3 = false;
+        boolean eventMouseWheelUp = false;
+        boolean eventMouseWheelDown = false;
+
+        // System.err.printf("buttons: %04x\r\n", buttons);
+
+        switch (buttons) {
+        case 0:
+            eventMouse1 = true;
+            mouse1 = true;
+            break;
+        case 1:
+            eventMouse2 = true;
+            mouse2 = true;
+            break;
+        case 2:
+            eventMouse3 = true;
+            mouse3 = true;
+            break;
+        case 3:
+            // Release or Move
+            if (!mouse1 && !mouse2 && !mouse3) {
+                eventType = TMouseEvent.Type.MOUSE_MOTION;
+            } else {
+                eventType = TMouseEvent.Type.MOUSE_UP;
+            }
+            if (mouse1) {
+                mouse1 = false;
+                eventMouse1 = true;
+            }
+            if (mouse2) {
+                mouse2 = false;
+                eventMouse2 = true;
+            }
+            if (mouse3) {
+                mouse3 = false;
+                eventMouse3 = true;
+            }
+            break;
+
+        case 32:
+            // Dragging with mouse1 down
+            eventMouse1 = true;
+            mouse1 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 33:
+            // Dragging with mouse2 down
+            eventMouse2 = true;
+            mouse2 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 34:
+            // Dragging with mouse3 down
+            eventMouse3 = true;
+            mouse3 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 96:
+            // Dragging with mouse2 down after wheelUp
+            eventMouse2 = true;
+            mouse2 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 97:
+            // Dragging with mouse2 down after wheelDown
+            eventMouse2 = true;
+            mouse2 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 64:
+            eventMouseWheelUp = true;
+            break;
+
+        case 65:
+            eventMouseWheelDown = true;
+            break;
+
+        default:
+            // Unknown, just make it motion
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+        }
+        return new TMouseEvent(eventType, x, y, x, y,
+            eventMouse1, eventMouse2, eventMouse3,
+            eventMouseWheelUp, eventMouseWheelDown);
+    }
+
+    /**
+     * Produce mouse events based on "Any event tracking" and SGR
+     * coordinates.  See
+     * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
+     *
+     * @param release if true, this was a release ('m')
+     * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
+     */
+    private TInputEvent parseMouseSGR(final boolean release) {
+        // SGR extended coordinates - mode 1006
+        if (params.size() < 3) {
+            // Invalid position, bail out.
+            return null;
+        }
+        int buttons = Integer.parseInt(params.get(0));
+        int x = Integer.parseInt(params.get(1)) - 1;
+        int y = Integer.parseInt(params.get(2)) - 1;
+
+        // Clamp X and Y to the physical screen coordinates.
+        if (x >= windowResize.getWidth()) {
+            x = windowResize.getWidth() - 1;
+        }
+        if (y >= windowResize.getHeight()) {
+            y = windowResize.getHeight() - 1;
+        }
+
+        TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
+        boolean eventMouse1 = false;
+        boolean eventMouse2 = false;
+        boolean eventMouse3 = false;
+        boolean eventMouseWheelUp = false;
+        boolean eventMouseWheelDown = false;
+
+        if (release) {
+            eventType = TMouseEvent.Type.MOUSE_UP;
+        }
+
+        switch (buttons) {
+        case 0:
+            eventMouse1 = true;
+            break;
+        case 1:
+            eventMouse2 = true;
+            break;
+        case 2:
+            eventMouse3 = true;
+            break;
+        case 35:
+            // Motion only, no buttons down
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 32:
+            // Dragging with mouse1 down
+            eventMouse1 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 33:
+            // Dragging with mouse2 down
+            eventMouse2 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 34:
+            // Dragging with mouse3 down
+            eventMouse3 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 96:
+            // Dragging with mouse2 down after wheelUp
+            eventMouse2 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 97:
+            // Dragging with mouse2 down after wheelDown
+            eventMouse2 = true;
+            eventType = TMouseEvent.Type.MOUSE_MOTION;
+            break;
+
+        case 64:
+            eventMouseWheelUp = true;
+            break;
+
+        case 65:
+            eventMouseWheelDown = true;
+            break;
+
+        default:
+            // Unknown, bail out
+            return null;
+        }
+        return new TMouseEvent(eventType, x, y, x, y,
+            eventMouse1, eventMouse2, eventMouse3,
+            eventMouseWheelUp, eventMouseWheelDown);
     }
 
     /**
      * Return any events in the IO queue.
      *
-     * @return list of new events (which may be empty)
+     * @param queue list to append new events to
      */
-    public List<TInputEvent> getEvents() {
-       List<TInputEvent> events = new LinkedList<TInputEvent>();
-       return events;
+    public void getEvents(final List<TInputEvent> queue) {
+        synchronized (eventQueue) {
+            if (eventQueue.size() > 0) {
+                synchronized (queue) {
+                    queue.addAll(eventQueue);
+                }
+                eventQueue.clear();
+            }
+        }
     }
 
     /**
-     * Parses the next character of input to see if an InputEvent is fully
-     * here.
+     * Return any events in the IO queue due to timeout.
      *
-     * @param ch Unicode code point
-     * @return list of new events (which may be empty)
+     * @param queue list to append new events to
      */
-    public List<TInputEvent> getEvents(char ch) {
-       return getEvents(ch, false);
+    private void getIdleEvents(final List<TInputEvent> queue) {
+        Date now = new Date();
+
+        // Check for new window size
+        long windowSizeDelay = now.getTime() - windowSizeTime;
+        if (windowSizeDelay > 1000) {
+            sessionInfo.queryWindowSize();
+            int newWidth = sessionInfo.getWindowWidth();
+            int newHeight = sessionInfo.getWindowHeight();
+            if ((newWidth != windowResize.getWidth())
+                || (newHeight != windowResize.getHeight())
+            ) {
+                TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
+                    newWidth, newHeight);
+                windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
+                    newWidth, newHeight);
+                queue.add(event);
+            }
+            windowSizeTime = now.getTime();
+        }
+
+        // ESCDELAY type timeout
+        if (state == ParseState.ESCAPE) {
+            long escDelay = now.getTime() - escapeTime;
+            if (escDelay > 100) {
+                // After 0.1 seconds, assume a true escape character
+                queue.add(controlChar((char)0x1B, false));
+                reset();
+            }
+        }
     }
 
     /**
      * Parses the next character of input to see if an InputEvent is
      * fully here.
      *
+     * @param events list to append new events to
      * @param ch Unicode code point
-     * @param noChar if true, ignore ch.  This is currently used to return a
-     * bare ESC and RESIZE events.
-     * @return list of new events (which may be empty)
      */
-    public List<TInputEvent> getEvents(char ch, boolean noChar) {
-       List<TInputEvent> events = new LinkedList<TInputEvent>();
-
-       TKeypressEvent keypress;
-       Date now = new Date();
-
-       // ESCDELAY type timeout
-       if (state == ParseState.ESCAPE) {
-           long escDelay = now.getTime() - escapeTime;
-           if (escDelay > 250) {
-               // After 0.25 seconds, assume a true escape character
-               events.add(controlChar((char)0x1B));
-               reset();
-           }
-       }
-
-       if (noChar == true) {
-           int newWidth = session.getWindowWidth();
-           int newHeight = session.getWindowHeight();
-           if ((newWidth != windowResize.width) ||
-               (newHeight != windowResize.height)) {
-               TResizeEvent event = new TResizeEvent(TResizeEvent.Type.Screen,
-                   newWidth, newHeight);
-               windowResize.width = newWidth;
-               windowResize.height = newHeight;
-               events.add(event);
-           }
-
-           // Nothing else to do, bail out
-           return events;
-       }
-
-       // System.err.printf("state: %s ch %c\r\n", state, ch);
-
-       switch (state) {
-       case GROUND:
-
-           if (ch == 0x1B) {
-               state = ParseState.ESCAPE;
-               escapeTime = now.getTime();
-               return events;
-           }
-
-           if (ch <= 0x1F) {
-               // Control character
-               events.add(controlChar(ch));
-               reset();
-               return events;
-           }
-
-           if (ch >= 0x20) {
-               // Normal character
-               keypress = new TKeypressEvent();
-               keypress.key.isKey = false;
-               keypress.key.ch = ch;
-               events.add(keypress);
-               reset();
-               return events;
-           }
-
-           break;
-
-       case ESCAPE:
-           if (ch <= 0x1F) {
-               // ALT-Control character
-               keypress = controlChar(ch);
-               keypress.key.alt = true;
-               events.add(keypress);
-               reset();
-               return events;
-           }
-
-           if (ch == 'O') {
-               // This will be one of the function keys
-               state = ParseState.ESCAPE_INTERMEDIATE;
-               return events;
-           }
-
-           // '[' goes to CSI_ENTRY
-           if (ch == '[') {
-               state = ParseState.CSI_ENTRY;
-               return events;
-           }
-
-           // Everything else is assumed to be Alt-keystroke
-           keypress = new TKeypressEvent();
-           keypress.key.isKey = false;
-           keypress.key.ch = ch;
-           keypress.key.alt = true;
-           if ((ch >= 'A') && (ch <= 'Z')) {
-               keypress.key.shift = true;
-           }
-           events.add(keypress);
-           reset();
-           return events;
-
-       case ESCAPE_INTERMEDIATE:
-           if ((ch >= 'P') && (ch <= 'S')) {
-               // Function key
-               keypress = new TKeypressEvent();
-               keypress.key.isKey = true;
-               switch (ch) {
-               case 'P':
-                   keypress.key.fnKey = TKeypress.F1;
-                   break;
-               case 'Q':
-                   keypress.key.fnKey = TKeypress.F2;
-                   break;
-               case 'R':
-                   keypress.key.fnKey = TKeypress.F3;
-                   break;
-               case 'S':
-                   keypress.key.fnKey = TKeypress.F4;
-                   break;
-               default:
-                   break;
-               }
-               events.add(keypress);
-               reset();
-               return events;
-           }
-
-           // Unknown keystroke, ignore
-           reset();
-           return events;
-
-       case CSI_ENTRY:
-           // Numbers - parameter values
-           if ((ch >= '0') && (ch <= '9')) {
-               params.set(paramI, params.get(paramI) + ch);
-               state = ParseState.CSI_PARAM;
-               return events;
-           }
-           // Parameter separator
-           if (ch == ';') {
-               paramI++;
-               params.set(paramI, "");
-               return events;
-           }
-
-           if ((ch >= 0x30) && (ch <= 0x7E)) {
-               switch (ch) {
-               case 'A':
-                   // Up
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.UP;
-                   if (params.size() > 1) {
-                       if (params.get(1).equals("2")) {
-                           keypress.key.shift = true;
-                       }
-                       if (params.get(1).equals("5")) {
-                           keypress.key.ctrl = true;
-                       }
-                       if (params.get(1).equals("3")) {
-                           keypress.key.alt = true;
-                       }
-                   }
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'B':
-                   // Down
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.DOWN;
-                   if (params.size() > 1) {
-                       if (params.get(1).equals("2")) {
-                           keypress.key.shift = true;
-                       }
-                       if (params.get(1).equals("5")) {
-                           keypress.key.ctrl = true;
-                       }
-                       if (params.get(1).equals("3")) {
-                           keypress.key.alt = true;
-                       }
-                   }
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'C':
-                   // Right
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.RIGHT;
-                   if (params.size() > 1) {
-                       if (params.get(1).equals("2")) {
-                           keypress.key.shift = true;
-                       }
-                       if (params.get(1).equals("5")) {
-                           keypress.key.ctrl = true;
-                       }
-                       if (params.get(1).equals("3")) {
-                           keypress.key.alt = true;
-                       }
-                   }
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'D':
-                   // Left
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.LEFT;
-                   if (params.size() > 1) {
-                       if (params.get(1).equals("2")) {
-                           keypress.key.shift = true;
-                       }
-                       if (params.get(1).equals("5")) {
-                           keypress.key.ctrl = true;
-                       }
-                       if (params.get(1).equals("3")) {
-                           keypress.key.alt = true;
-                       }
-                   }
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'H':
-                   // Home
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.HOME;
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'F':
-                   // End
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.END;
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'Z':
-                   // CBT - Cursor backward X tab stops (default 1)
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.BTAB;
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'M':
-                   // Mouse position
-                   state = ParseState.MOUSE;
-                   return events;
-               default:
-                   break;
-               }
-           }
-
-           // Unknown keystroke, ignore
-           reset();
-           return events;
-
-       case CSI_PARAM:
-           // Numbers - parameter values
-           if ((ch >= '0') && (ch <= '9')) {
-               params.set(paramI, params.get(paramI) + ch);
-               state = ParseState.CSI_PARAM;
-               return events;
-           }
-           // Parameter separator
-           if (ch == ';') {
-               paramI++;
-               params.set(paramI, "");
-               return events;
-           }
-
-           if (ch == '~') {
-               events.add(csiFnKey());
-               reset();
-               return events;
-           }
-
-           if ((ch >= 0x30) && (ch <= 0x7E)) {
-               switch (ch) {
-               case 'A':
-                   // Up
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.UP;
-                   if (params.size() > 1) {
-                       if (params.get(1).equals("2")) {
-                           keypress.key.shift = true;
-                       }
-                       if (params.get(1).equals("5")) {
-                           keypress.key.ctrl = true;
-                       }
-                       if (params.get(1).equals("3")) {
-                           keypress.key.alt = true;
-                       }
-                   }
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'B':
-                   // Down
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.DOWN;
-                   if (params.size() > 1) {
-                       if (params.get(1).equals("2")) {
-                           keypress.key.shift = true;
-                       }
-                       if (params.get(1).equals("5")) {
-                           keypress.key.ctrl = true;
-                       }
-                       if (params.get(1).equals("3")) {
-                           keypress.key.alt = true;
-                       }
-                   }
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'C':
-                   // Right
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.RIGHT;
-                   if (params.size() > 1) {
-                       if (params.get(1).equals("2")) {
-                           keypress.key.shift = true;
-                       }
-                       if (params.get(1).equals("5")) {
-                           keypress.key.ctrl = true;
-                       }
-                       if (params.get(1).equals("3")) {
-                           keypress.key.alt = true;
-                       }
-                   }
-                   events.add(keypress);
-                   reset();
-                   return events;
-               case 'D':
-                   // Left
-                   keypress = new TKeypressEvent();
-                   keypress.key.isKey = true;
-                   keypress.key.fnKey = TKeypress.LEFT;
-                   if (params.size() > 1) {
-                       if (params.get(1).equals("2")) {
-                           keypress.key.shift = true;
-                       }
-                       if (params.get(1).equals("5")) {
-                           keypress.key.ctrl = true;
-                       }
-                       if (params.get(1).equals("3")) {
-                           keypress.key.alt = true;
-                       }
-                   }
-                   events.add(keypress);
-                   reset();
-                   return events;
-               default:
-                   break;
-               }
-           }
-
-           // Unknown keystroke, ignore
-           reset();
-           return events;
-
-       case MOUSE:
-           params.set(0, params.get(paramI) + ch);
-           if (params.get(0).length() == 3) {
-               // We have enough to generate a mouse event
-               events.add(parseMouse());
-               reset();
-           }
-           return events;
-
-       default:
-           break;
-       }
-
-       // This "should" be impossible to reach
-       return events;
+    private void processChar(final List<TInputEvent> events, final char ch) {
+
+        // ESCDELAY type timeout
+        Date now = new Date();
+        if (state == ParseState.ESCAPE) {
+            long escDelay = now.getTime() - escapeTime;
+            if (escDelay > 250) {
+                // After 0.25 seconds, assume a true escape character
+                events.add(controlChar((char)0x1B, false));
+                reset();
+            }
+        }
+
+        // TKeypress fields
+        boolean ctrl = false;
+        boolean alt = false;
+        boolean shift = false;
+
+        // System.err.printf("state: %s ch %c\r\n", state, ch);
+
+        switch (state) {
+        case GROUND:
+
+            if (ch == 0x1B) {
+                state = ParseState.ESCAPE;
+                escapeTime = now.getTime();
+                return;
+            }
+
+            if (ch <= 0x1F) {
+                // Control character
+                events.add(controlChar(ch, false));
+                reset();
+                return;
+            }
+
+            if (ch >= 0x20) {
+                // Normal character
+                events.add(new TKeypressEvent(false, 0, ch,
+                        false, false, false));
+                reset();
+                return;
+            }
+
+            break;
+
+        case ESCAPE:
+            if (ch <= 0x1F) {
+                // ALT-Control character
+                events.add(controlChar(ch, true));
+                reset();
+                return;
+            }
+
+            if (ch == 'O') {
+                // This will be one of the function keys
+                state = ParseState.ESCAPE_INTERMEDIATE;
+                return;
+            }
+
+            // '[' goes to CSI_ENTRY
+            if (ch == '[') {
+                state = ParseState.CSI_ENTRY;
+                return;
+            }
+
+            // Everything else is assumed to be Alt-keystroke
+            if ((ch >= 'A') && (ch <= 'Z')) {
+                shift = true;
+            }
+            alt = true;
+            events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
+            reset();
+            return;
+
+        case ESCAPE_INTERMEDIATE:
+            if ((ch >= 'P') && (ch <= 'S')) {
+                // Function key
+                switch (ch) {
+                case 'P':
+                    events.add(new TKeypressEvent(kbF1));
+                    break;
+                case 'Q':
+                    events.add(new TKeypressEvent(kbF2));
+                    break;
+                case 'R':
+                    events.add(new TKeypressEvent(kbF3));
+                    break;
+                case 'S':
+                    events.add(new TKeypressEvent(kbF4));
+                    break;
+                default:
+                    break;
+                }
+                reset();
+                return;
+            }
+
+            // Unknown keystroke, ignore
+            reset();
+            return;
+
+        case CSI_ENTRY:
+            // Numbers - parameter values
+            if ((ch >= '0') && (ch <= '9')) {
+                params.set(params.size() - 1,
+                    params.get(params.size() - 1) + ch);
+                state = ParseState.CSI_PARAM;
+                return;
+            }
+            // Parameter separator
+            if (ch == ';') {
+                params.add("");
+                return;
+            }
+
+            if ((ch >= 0x30) && (ch <= 0x7E)) {
+                switch (ch) {
+                case 'A':
+                    // Up
+                    if (params.size() > 1) {
+                        if (params.get(1).equals("2")) {
+                            shift = true;
+                        }
+                        if (params.get(1).equals("5")) {
+                            ctrl = true;
+                        }
+                        if (params.get(1).equals("3")) {
+                            alt = true;
+                        }
+                    }
+                    events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
+                    reset();
+                    return;
+                case 'B':
+                    // Down
+                    if (params.size() > 1) {
+                        if (params.get(1).equals("2")) {
+                            shift = true;
+                        }
+                        if (params.get(1).equals("5")) {
+                            ctrl = true;
+                        }
+                        if (params.get(1).equals("3")) {
+                            alt = true;
+                        }
+                    }
+                    events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
+                    reset();
+                    return;
+                case 'C':
+                    // Right
+                    if (params.size() > 1) {
+                        if (params.get(1).equals("2")) {
+                            shift = true;
+                        }
+                        if (params.get(1).equals("5")) {
+                            ctrl = true;
+                        }
+                        if (params.get(1).equals("3")) {
+                            alt = true;
+                        }
+                    }
+                    events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
+                    reset();
+                    return;
+                case 'D':
+                    // Left
+                    if (params.size() > 1) {
+                        if (params.get(1).equals("2")) {
+                            shift = true;
+                        }
+                        if (params.get(1).equals("5")) {
+                            ctrl = true;
+                        }
+                        if (params.get(1).equals("3")) {
+                            alt = true;
+                        }
+                    }
+                    events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
+                    reset();
+                    return;
+                case 'H':
+                    // Home
+                    events.add(new TKeypressEvent(kbHome));
+                    reset();
+                    return;
+                case 'F':
+                    // End
+                    events.add(new TKeypressEvent(kbEnd));
+                    reset();
+                    return;
+                case 'Z':
+                    // CBT - Cursor backward X tab stops (default 1)
+                    events.add(new TKeypressEvent(kbBackTab));
+                    reset();
+                    return;
+                case 'M':
+                    // Mouse position
+                    state = ParseState.MOUSE;
+                    return;
+                case '<':
+                    // Mouse position, SGR (1006) coordinates
+                    state = ParseState.MOUSE_SGR;
+                    return;
+                default:
+                    break;
+                }
+            }
+
+            // Unknown keystroke, ignore
+            reset();
+            return;
+
+        case MOUSE_SGR:
+            // Numbers - parameter values
+            if ((ch >= '0') && (ch <= '9')) {
+                params.set(params.size() - 1,
+                    params.get(params.size() - 1) + ch);
+                return;
+            }
+            // Parameter separator
+            if (ch == ';') {
+                params.add("");
+                return;
+            }
+
+            switch (ch) {
+            case 'M':
+                // Generate a mouse press event
+                TInputEvent event = parseMouseSGR(false);
+                if (event != null) {
+                    events.add(event);
+                }
+                reset();
+                return;
+            case 'm':
+                // Generate a mouse release event
+                event = parseMouseSGR(true);
+                if (event != null) {
+                    events.add(event);
+                }
+                reset();
+                return;
+            default:
+                break;
+            }
+
+            // Unknown keystroke, ignore
+            reset();
+            return;
+
+        case CSI_PARAM:
+            // Numbers - parameter values
+            if ((ch >= '0') && (ch <= '9')) {
+                params.set(params.size() - 1,
+                    params.get(params.size() - 1) + ch);
+                state = ParseState.CSI_PARAM;
+                return;
+            }
+            // Parameter separator
+            if (ch == ';') {
+                params.add("");
+                return;
+            }
+
+            if (ch == '~') {
+                events.add(csiFnKey());
+                reset();
+                return;
+            }
+
+            if ((ch >= 0x30) && (ch <= 0x7E)) {
+                switch (ch) {
+                case 'A':
+                    // Up
+                    if (params.size() > 1) {
+                        if (params.get(1).equals("2")) {
+                            shift = true;
+                        }
+                        if (params.get(1).equals("5")) {
+                            ctrl = true;
+                        }
+                        if (params.get(1).equals("3")) {
+                            alt = true;
+                        }
+                    }
+                    events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
+                    reset();
+                    return;
+                case 'B':
+                    // Down
+                    if (params.size() > 1) {
+                        if (params.get(1).equals("2")) {
+                            shift = true;
+                        }
+                        if (params.get(1).equals("5")) {
+                            ctrl = true;
+                        }
+                        if (params.get(1).equals("3")) {
+                            alt = true;
+                        }
+                    }
+                    events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
+                    reset();
+                    return;
+                case 'C':
+                    // Right
+                    if (params.size() > 1) {
+                        if (params.get(1).equals("2")) {
+                            shift = true;
+                        }
+                        if (params.get(1).equals("5")) {
+                            ctrl = true;
+                        }
+                        if (params.get(1).equals("3")) {
+                            alt = true;
+                        }
+                    }
+                    events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
+                    reset();
+                    return;
+                case 'D':
+                    // Left
+                    if (params.size() > 1) {
+                        if (params.get(1).equals("2")) {
+                            shift = true;
+                        }
+                        if (params.get(1).equals("5")) {
+                            ctrl = true;
+                        }
+                        if (params.get(1).equals("3")) {
+                            alt = true;
+                        }
+                    }
+                    events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
+                    reset();
+                    return;
+                default:
+                    break;
+                }
+            }
+
+            // Unknown keystroke, ignore
+            reset();
+            return;
+
+        case MOUSE:
+            params.set(0, params.get(params.size() - 1) + ch);
+            if (params.get(0).length() == 3) {
+                // We have enough to generate a mouse event
+                events.add(parseMouse());
+                reset();
+            }
+            return;
+
+        default:
+            break;
+        }
+
+        // This "should" be impossible to reach
+        return;
     }
 
     /**
@@ -1112,11 +1158,11 @@ public class ECMA48Terminal {
      * @param on if true, enable metaSendsEscape
      * @return the string to emit to xterm
      */
-    static public String xtermMetaSendsEscape(boolean on) {
-       if (on) {
-           return "\033[?1036h\033[?1034l";
-       }
-       return "\033[?1036l";
+    private String xtermMetaSendsEscape(final boolean on) {
+        if (on) {
+            return "\033[?1036h\033[?1034l";
+        }
+        return "\033[?1036l";
     }
 
     /**
@@ -1128,26 +1174,27 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[31;1m"
      */
-    static public String addHeaderSGR(String str) {
-       if (str.length() > 0) {
-           // Nix any trailing ';' because that resets all attributes
-           while (str.endsWith(":")) {
-               str = str.substring(0, str.length() - 1);
-           }
-       }
-       return "\033[" + str + "m";
+    private String addHeaderSGR(String str) {
+        if (str.length() > 0) {
+            // Nix any trailing ';' because that resets all attributes
+            while (str.endsWith(":")) {
+                str = str.substring(0, str.length() - 1);
+            }
+        }
+        return "\033[" + str + "m";
     }
 
     /**
-     * Create a SGR parameter sequence for a single color change.
+     * Create a SGR parameter sequence for a single color change.  Note
+     * package private access.
      *
      * @param color one of the Color.WHITE, Color.BLUE, etc. constants
      * @param foreground if true, this is a foreground color
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[42m"
      */
-    static public String color(Color color, boolean foreground) {
-       return color(color, foreground, true);
+    String color(final Color color, final boolean foreground) {
+        return color(color, foreground, true);
     }
 
     /**
@@ -1160,36 +1207,36 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[42m"
      */
-    static public String color(Color color, boolean foreground,
-       boolean header) {
-
-       int ecmaColor = color.value;
-
-       // Convert Color.* values to SGR numerics
-       if (foreground == true) {
-           ecmaColor += 30;
-       } else {
-           ecmaColor += 40;
-       }
-
-       if (header) {
-           return String.format("\033[%dm", ecmaColor);
-       } else {
-           return String.format("%d;", ecmaColor);
-       }
+    private String color(final Color color, final boolean foreground,
+        final boolean header) {
+
+        int ecmaColor = color.getValue();
+
+        // Convert Color.* values to SGR numerics
+        if (foreground) {
+            ecmaColor += 30;
+        } else {
+            ecmaColor += 40;
+        }
+
+        if (header) {
+            return String.format("\033[%dm", ecmaColor);
+        } else {
+            return String.format("%d;", ecmaColor);
+        }
     }
 
     /**
-     * Create a SGR parameter sequence for both foreground and
-     * background color change.
+     * Create a SGR parameter sequence for both foreground and background
+     * color change.  Note package private access.
      *
      * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
      * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[31;42m"
      */
-    static public String color(Color foreColor, Color backColor) {
-       return color(foreColor, backColor, true);
+    String color(final Color foreColor, final Color backColor) {
+        return color(foreColor, backColor, true);
     }
 
     /**
@@ -1203,27 +1250,28 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[31;42m"
      */
-    static public String color(Color foreColor, Color backColor,
-       boolean header) {
+    private String color(final Color foreColor, final Color backColor,
+        final boolean header) {
 
-       int ecmaForeColor = foreColor.value;
-       int ecmaBackColor = backColor.value;
+        int ecmaForeColor = foreColor.getValue();
+        int ecmaBackColor = backColor.getValue();
 
-       // Convert Color.* values to SGR numerics
-       ecmaBackColor += 40;
-       ecmaForeColor += 30;
+        // Convert Color.* values to SGR numerics
+        ecmaBackColor += 40;
+        ecmaForeColor += 30;
 
-       if (header) {
-           return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
-       } else {
-           return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
-       }
+        if (header) {
+            return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor);
+        } else {
+            return String.format("%d;%d;", ecmaForeColor, ecmaBackColor);
+        }
     }
 
     /**
      * Create a SGR parameter sequence for foreground, background, and
      * several attributes.  This sequence first resets all attributes to
-     * default, then sets attributes as per the parameters.
+     * default, then sets attributes as per the parameters.  Note package
+     * private access.
      *
      * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
      * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
@@ -1234,53 +1282,54 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[0;1;31;42m"
      */
-    static public String color(Color foreColor, Color backColor, boolean bold,
-       boolean reverse, boolean blink, boolean underline) {
-
-       int ecmaForeColor = foreColor.value;
-       int ecmaBackColor = backColor.value;
-
-       // Convert Color.* values to SGR numerics
-       ecmaBackColor += 40;
-       ecmaForeColor += 30;
-
-       StringBuilder sb = new StringBuilder();
-       if        (  bold &&  reverse &&  blink && !underline ) {
-           sb.append("\033[0;1;7;5;");
-       } else if (  bold &&  reverse && !blink && !underline ) {
-           sb.append("\033[0;1;7;");
-       } else if ( !bold &&  reverse &&  blink && !underline ) {
-           sb.append("\033[0;7;5;");
-       } else if (  bold && !reverse &&  blink && !underline ) {
-           sb.append("\033[0;1;5;");
-       } else if (  bold && !reverse && !blink && !underline ) {
-           sb.append("\033[0;1;");
-       } else if ( !bold &&  reverse && !blink && !underline ) {
-           sb.append("\033[0;7;");
-       } else if ( !bold && !reverse &&  blink && !underline) {
-           sb.append("\033[0;5;");
-       } else if (  bold &&  reverse &&  blink &&  underline ) {
-           sb.append("\033[0;1;7;5;4;");
-       } else if (  bold &&  reverse && !blink &&  underline ) {
-           sb.append("\033[0;1;7;4;");
-       } else if ( !bold &&  reverse &&  blink &&  underline ) {
-           sb.append("\033[0;7;5;4;");
-       } else if (  bold && !reverse &&  blink &&  underline ) {
-           sb.append("\033[0;1;5;4;");
-       } else if (  bold && !reverse && !blink &&  underline ) {
-           sb.append("\033[0;1;4;");
-       } else if ( !bold &&  reverse && !blink &&  underline ) {
-           sb.append("\033[0;7;4;");
-       } else if ( !bold && !reverse &&  blink &&  underline) {
-           sb.append("\033[0;5;4;");
-       } else if ( !bold && !reverse && !blink &&  underline) {
-           sb.append("\033[0;4;");
-       } else {
-           assert(!bold && !reverse && !blink && !underline);
-           sb.append("\033[0;");
-       }
-       sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
-       return sb.toString();
+    String color(final Color foreColor, final Color backColor,
+        final boolean bold, final boolean reverse, final boolean blink,
+        final boolean underline) {
+
+        int ecmaForeColor = foreColor.getValue();
+        int ecmaBackColor = backColor.getValue();
+
+        // Convert Color.* values to SGR numerics
+        ecmaBackColor += 40;
+        ecmaForeColor += 30;
+
+        StringBuilder sb = new StringBuilder();
+        if        (  bold &&  reverse &&  blink && !underline ) {
+            sb.append("\033[0;1;7;5;");
+        } else if (  bold &&  reverse && !blink && !underline ) {
+            sb.append("\033[0;1;7;");
+        } else if ( !bold &&  reverse &&  blink && !underline ) {
+            sb.append("\033[0;7;5;");
+        } else if (  bold && !reverse &&  blink && !underline ) {
+            sb.append("\033[0;1;5;");
+        } else if (  bold && !reverse && !blink && !underline ) {
+            sb.append("\033[0;1;");
+        } else if ( !bold &&  reverse && !blink && !underline ) {
+            sb.append("\033[0;7;");
+        } else if ( !bold && !reverse &&  blink && !underline) {
+            sb.append("\033[0;5;");
+        } else if (  bold &&  reverse &&  blink &&  underline ) {
+            sb.append("\033[0;1;7;5;4;");
+        } else if (  bold &&  reverse && !blink &&  underline ) {
+            sb.append("\033[0;1;7;4;");
+        } else if ( !bold &&  reverse &&  blink &&  underline ) {
+            sb.append("\033[0;7;5;4;");
+        } else if (  bold && !reverse &&  blink &&  underline ) {
+            sb.append("\033[0;1;5;4;");
+        } else if (  bold && !reverse && !blink &&  underline ) {
+            sb.append("\033[0;1;4;");
+        } else if ( !bold &&  reverse && !blink &&  underline ) {
+            sb.append("\033[0;7;4;");
+        } else if ( !bold && !reverse &&  blink &&  underline) {
+            sb.append("\033[0;5;4;");
+        } else if ( !bold && !reverse && !blink &&  underline) {
+            sb.append("\033[0;4;");
+        } else {
+            assert (!bold && !reverse && !blink && !underline);
+            sb.append("\033[0;");
+        }
+        sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor));
+        return sb.toString();
     }
 
     /**
@@ -1290,21 +1339,22 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[7m"
      */
-    static public String reverse(boolean on) {
-       if (on) {
-           return "\033[7m";
-       }
-       return "\033[27m";
+    private String reverse(final boolean on) {
+        if (on) {
+            return "\033[7m";
+        }
+        return "\033[27m";
     }
 
     /**
-     * Create a SGR parameter sequence to reset to defaults.
+     * Create a SGR parameter sequence to reset to defaults.  Note package
+     * private access.
      *
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[0m"
      */
-    static public String normal() {
-       return normal(true);
+    String normal() {
+        return normal(true);
     }
 
     /**
@@ -1315,11 +1365,11 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[0m"
      */
-    static public String normal(boolean header) {
-       if (header) {
-           return "\033[0;37;40m";
-       }
-       return "0;37;40";
+    private String normal(final boolean header) {
+        if (header) {
+            return "\033[0;37;40m";
+        }
+        return "0;37;40";
     }
 
     /**
@@ -1329,8 +1379,8 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[1m"
      */
-    static public String bold(boolean on) {
-       return bold(on, true);
+    private String bold(final boolean on) {
+        return bold(on, true);
     }
 
     /**
@@ -1342,17 +1392,17 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[1m"
      */
-    static public String bold(boolean on, boolean header) {
-       if (header) {
-           if (on) {
-               return "\033[1m";
-           }
-           return "\033[22m";
-       }
-       if (on) {
-           return "1;";
-       }
-       return "22;";
+    private String bold(final boolean on, final boolean header) {
+        if (header) {
+            if (on) {
+                return "\033[1m";
+            }
+            return "\033[22m";
+        }
+        if (on) {
+            return "1;";
+        }
+        return "22;";
     }
 
     /**
@@ -1362,8 +1412,8 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[5m"
      */
-    static public String blink(boolean on) {
-       return blink(on, true);
+    private String blink(final boolean on) {
+        return blink(on, true);
     }
 
     /**
@@ -1375,17 +1425,17 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[5m"
      */
-    static public String blink(boolean on, boolean header) {
-       if (header) {
-           if (on) {
-               return "\033[5m";
-           }
-           return "\033[25m";
-       }
-       if (on) {
-           return "5;";
-       }
-       return "25;";
+    private String blink(final boolean on, final boolean header) {
+        if (header) {
+            if (on) {
+                return "\033[5m";
+            }
+            return "\033[25m";
+        }
+        if (on) {
+            return "5;";
+        }
+        return "25;";
     }
 
     /**
@@ -1396,29 +1446,30 @@ public class ECMA48Terminal {
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[4m"
      */
-    static public String underline(boolean on) {
-       if (on) {
-           return "\033[4m";
-       }
-       return "\033[24m";
+    private String underline(final boolean on) {
+        if (on) {
+            return "\033[4m";
+        }
+        return "\033[24m";
     }
 
     /**
-     * Create a SGR parameter sequence for enabling the visible cursor.
+     * Create a SGR parameter sequence for enabling the visible cursor.  Note
+     * package private access.
      *
      * @param on if true, turn on cursor
      * @return the string to emit to an ANSI / ECMA-style terminal
      */
-    public String cursor(boolean on) {
-       if (on && (cursorOn == false)) {
-           cursorOn = true;
-           return "\033[?25h";
-       }
-       if (!on && (cursorOn == true)) {
-           cursorOn = false;
-           return "\033[?25l";
-       }
-       return "";
+    String cursor(final boolean on) {
+        if (on && !cursorOn) {
+            cursorOn = true;
+            return "\033[?25h";
+        }
+        if (!on && cursorOn) {
+            cursorOn = false;
+            return "\033[?25l";
+        }
+        return "";
     }
 
     /**
@@ -1427,19 +1478,19 @@ public class ECMA48Terminal {
      *
      * @return the string to emit to an ANSI / ECMA-style terminal
      */
-    static public String clearAll() {
-       return "\033[0;37;40m\033[2J";
+    public String clearAll() {
+        return "\033[0;37;40m\033[2J";
     }
 
     /**
      * Clear the line from the cursor (inclusive) to the end of the screen.
      * Because some terminals use back-color-erase, set the color to
-     * white-on-black beforehand.
+     * white-on-black beforehand.  Note package private access.
      *
      * @return the string to emit to an ANSI / ECMA-style terminal
      */
-    static public String clearRemainingLine() {
-       return "\033[0;37;40m\033[K";
+    String clearRemainingLine() {
+        return "\033[0;37;40m\033[K";
     }
 
     /**
@@ -1448,8 +1499,8 @@ public class ECMA48Terminal {
      *
      * @return the string to emit to an ANSI / ECMA-style terminal
      */
-    static public String clearPreceedingLine() {
-       return "\033[0;37;40m\033[1K";
+    private String clearPreceedingLine() {
+        return "\033[0;37;40m\033[1K";
     }
 
     /**
@@ -1458,8 +1509,8 @@ public class ECMA48Terminal {
      *
      * @return the string to emit to an ANSI / ECMA-style terminal
      */
-    static public String clearLine() {
-       return "\033[0;37;40m\033[2K";
+    private String clearLine() {
+        return "\033[0;37;40m\033[2K";
     }
 
     /**
@@ -1467,24 +1518,26 @@ public class ECMA48Terminal {
      *
      * @return the string to emit to an ANSI / ECMA-style terminal
      */
-    static public String home() {
-       return "\033[H";
+    private String home() {
+        return "\033[H";
     }
 
     /**
-     * Move the cursor to (x, y).
+     * Move the cursor to (x, y).  Note package private access.
      *
      * @param x column coordinate.  0 is the left-most column.
      * @param y row coordinate.  0 is the top-most row.
      * @return the string to emit to an ANSI / ECMA-style terminal
      */
-    static public String gotoXY(int x, int y) {
-       return String.format("\033[%d;%dH", y + 1, x + 1);
+    String gotoXY(final int x, final int y) {
+        return String.format("\033[%d;%dH", y + 1, x + 1);
     }
 
     /**
      * Tell (u)xterm that we want to receive mouse events based on "Any event
-     * tracking" and UTF-8 coordinates.  See
+     * tracking", UTF-8 coordinates, and then SGR coordinates.  Ideally we
+     * will end up with SGR coordinates with UTF-8 coordinates as a fallback.
+     * See
      * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
      *
      * Note that this also sets the alternate/primary screen buffer.
@@ -1494,11 +1547,81 @@ public class ECMA48Terminal {
      * buffer.
      * @return the string to emit to xterm
      */
-    static public String mouse(boolean on) {
-       if (on) {
-           return "\033[?1003;1005h\033[?1049h";
-       }
-       return "\033[?1003;1005l\033[?1049l";
+    private String mouse(final boolean on) {
+        if (on) {
+            return "\033[?1003;1005;1006h\033[?1049h";
+        }
+        return "\033[?1003;1006;1005l\033[?1049l";
+    }
+
+    /**
+     * Read function runs on a separate thread.
+     */
+    public void run() {
+        boolean done = false;
+        // available() will often return > 1, so we need to read in chunks to
+        // stay caught up.
+        char [] readBuffer = new char[128];
+        List<TInputEvent> events = new LinkedList<TInputEvent>();
+
+        while (!done && !stopReaderThread) {
+            try {
+                // We assume that if inputStream has bytes available, then
+                // input won't block on read().
+                int n = inputStream.available();
+                if (n > 0) {
+                    if (readBuffer.length < n) {
+                        // The buffer wasn't big enough, make it huger
+                        readBuffer = new char[readBuffer.length * 2];
+                    }
+
+                    int rc = input.read(readBuffer, 0, readBuffer.length);
+                    // System.err.printf("read() %d", rc); System.err.flush();
+                    if (rc == -1) {
+                        // This is EOF
+                        done = true;
+                    } else {
+                        for (int i = 0; i < rc; i++) {
+                            int ch = readBuffer[i];
+                            processChar(events, (char)ch);
+                        }
+                        getIdleEvents(events);
+                        if (events.size() > 0) {
+                            // Add to the queue for the backend thread to
+                            // be able to obtain.
+                            synchronized (eventQueue) {
+                                eventQueue.addAll(events);
+                            }
+                            synchronized (listener) {
+                                listener.notifyAll();
+                            }
+                            events.clear();
+                        }
+                    }
+                } else {
+                    getIdleEvents(events);
+                    if (events.size() > 0) {
+                        synchronized (eventQueue) {
+                            eventQueue.addAll(events);
+                        }
+                        events.clear();
+                        synchronized (listener) {
+                            listener.notifyAll();
+                        }
+                    }
+
+                    // Wait 10 millis for more data
+                    Thread.sleep(10);
+                }
+                // System.err.println("end while loop"); System.err.flush();
+            } catch (InterruptedException e) {
+                // SQUASH
+            } catch (IOException e) {
+                e.printStackTrace();
+                done = true;
+            }
+        } // while ((done == false) && (stopReaderThread == false))
+        // System.err.println("*** run() exiting..."); System.err.flush();
     }
 
 }