terminal/backend handshaking, no more polling
[nikiroo-utils.git] / src / jexer / io / ECMA48Terminal.java
index f6ae12fdf30916c047d021e7f27d642d4ebc7817..d7d23915d0c416562d0d00874a86643e2fcc7464 100644 (file)
@@ -33,6 +33,8 @@
 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;
@@ -62,13 +64,28 @@ import static jexer.TKeypress.*;
  * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys,
  * etc.
  */
-public class ECMA48Terminal {
+public class ECMA48Terminal implements Runnable {
 
     /**
      * The session information
      */
     public SessionInfo session;
 
+    /**
+     * The event queue, filled up by a thread reading on input
+     */
+    private List<TInputEvent> eventQueue;
+
+    /**
+     * If true, we want the reader thread to exit gracefully.
+     */
+    private boolean stopReaderThread;
+
+    /**
+     * 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.
@@ -139,11 +156,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
@@ -159,6 +184,26 @@ public class ECMA48Terminal {
      */
     private boolean brokenTerminalUTFMouse = false;
 
+    /**
+     * Get the output writer.
+     *
+     * @return the Writer
+     */
+    public PrintWriter getOutput() {
+       return output;
+    }
+
+    /**
+     * 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 cooked' to set cooked mode.
      */
@@ -180,14 +225,12 @@ public class ECMA48Terminal {
      */
     private void doStty(boolean mode) {
        String [] cmdRaw = {
-           "/bin/sh", "-c", "stty raw < /dev/tty"
+           "/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 cooked < /dev/tty"
+           "/bin/sh", "-c", "stty sane cooked < /dev/tty"
        };
        try {
-           System.out.println("spawn stty");
-
            Process process;
            if (mode == true) {
                process = Runtime.getRuntime().exec(cmdRaw);
@@ -235,17 +278,21 @@ public class ECMA48Terminal {
     public ECMA48Terminal(InputStream input, OutputStream output) throws UnsupportedEncodingException {
 
        reset();
-       mouse1 = false;
-       mouse2 = false;
-       mouse3 = false;
+       mouse1           = false;
+       mouse2           = false;
+       mouse3           = false;
+       stopReaderThread = false;
 
        if (input == null) {
-           this.input = new InputStreamReader(System.in, "UTF-8");
+           // inputStream = System.in;
+           inputStream = new FileInputStream(FileDescriptor.in);
            sttyRaw();
            setRawMode = true;
        } else {
-           this.input = new InputStreamReader(input);
+           inputStream = input;
        }
+       this.input = new InputStreamReader(inputStream, "UTF-8");
+
        // TODO: include TelnetSocket from NIB and have it implement
        // SessionInfo
        if (input instanceof SessionInfo) {
@@ -274,18 +321,52 @@ public class ECMA48Terminal {
        // Hang onto the window size
        windowResize = new TResizeEvent(TResizeEvent.Type.Screen,
            session.getWindowWidth(), session.getWindowHeight());
+
+       // Spin up the input reader
+       eventQueue = new LinkedList<TInputEvent>();
+       readerThread = new Thread(this);
+       readerThread.start();
     }
 
     /**
      * Restore terminal to normal state
      */
     public void shutdown() {
+
+       // 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();
+           }
        }
-       // Disable mouse reporting and show cursor
-       output.printf("%s%s%s", mouse(false), cursor(true), normal());
     }
 
     /**
@@ -300,6 +381,7 @@ public class ECMA48Terminal {
      */
     private void reset() {
        state = ParseState.GROUND;
+       params = new ArrayList<String>();
        paramI = 0;
        params.clear();
        params.add("");
@@ -320,8 +402,12 @@ public class ECMA48Terminal {
        // System.err.printf("controlChar: %02x\n", ch);
 
        switch (ch) {
-       case '\r':
-           // ENTER
+       case 0x0D:
+           // Carriage return --> ENTER
+           event.key = kbEnter;
+           break;
+       case 0x0A:
+           // Linefeed --> ENTER
            event.key = kbEnter;
            break;
        case 0x1B:
@@ -687,35 +773,57 @@ public class ECMA48Terminal {
     }
 
     /**
-     * Parses the next character of input to see if an InputEvent is
-     * fully here.
+     * Return any events in the IO queue.
      *
-     * Params:
-     *    ch = Unicode code point
-     *    noChar = if true, ignore ch.  This is currently used to
-     *    return a bare ESC and RESIZE events.
+     * @param queue list to append new events to
+     */
+    public void getEvents(List<TInputEvent> queue) {
+       synchronized (eventQueue) {
+           if (eventQueue.size() > 0) {
+               queue.addAll(eventQueue);
+               eventQueue.clear();
+           }
+       }
+    }
+
+    /**
+     * Return any events in the IO queue due to timeout.
      *
-     * Returns:
-     *    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);
+    public void getIdleEvents(List<TInputEvent> queue) {
+
+       // Check for new window size
+       session.queryWindowSize();
+       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;
+           synchronized (eventQueue) {
+               eventQueue.add(event);
+           }
+       }
+
+       synchronized (eventQueue) {
+           if (eventQueue.size() > 0) {
+               queue.addAll(eventQueue);
+               eventQueue.clear();
+           }
+       }
     }
 
     /**
      * Parses the next character of input to see if an InputEvent is
      * fully here.
      *
-     * Params:
-     *    ch = Unicode code point
-     *    noChar = if true, ignore ch.  This is currently used to
-     *    return a bare ESC and RESIZE events.
-     *
-     * Returns:
-     *    list of new events (which may be empty)
+     * @param events list to append new events to
+     * @param ch Unicode code point
      */
-    public List<TInputEvent> getEvents(char ch, boolean noChar) {
-       List<TInputEvent> events = new LinkedList<TInputEvent>();
+    private void processChar(List<TInputEvent> events, char ch) {
 
        TKeypressEvent keypress;
        Date now = new Date();
@@ -730,22 +838,6 @@ public class ECMA48Terminal {
            }
        }
 
-       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) {
@@ -754,14 +846,14 @@ public class ECMA48Terminal {
            if (ch == 0x1B) {
                state = ParseState.ESCAPE;
                escapeTime = now.getTime();
-               return events;
+               return;
            }
 
            if (ch <= 0x1F) {
                // Control character
                events.add(controlChar(ch));
                reset();
-               return events;
+               return;
            }
 
            if (ch >= 0x20) {
@@ -771,7 +863,7 @@ public class ECMA48Terminal {
                keypress.key.ch = ch;
                events.add(keypress);
                reset();
-               return events;
+               return;
            }
 
            break;
@@ -783,19 +875,19 @@ public class ECMA48Terminal {
                keypress.key.alt = true;
                events.add(keypress);
                reset();
-               return events;
+               return;
            }
 
            if (ch == 'O') {
                // This will be one of the function keys
                state = ParseState.ESCAPE_INTERMEDIATE;
-               return events;
+               return;
            }
 
            // '[' goes to CSI_ENTRY
            if (ch == '[') {
                state = ParseState.CSI_ENTRY;
-               return events;
+               return;
            }
 
            // Everything else is assumed to be Alt-keystroke
@@ -808,7 +900,7 @@ public class ECMA48Terminal {
            }
            events.add(keypress);
            reset();
-           return events;
+           return;
 
        case ESCAPE_INTERMEDIATE:
            if ((ch >= 'P') && (ch <= 'S')) {
@@ -833,25 +925,25 @@ public class ECMA48Terminal {
                }
                events.add(keypress);
                reset();
-               return events;
+               return;
            }
 
            // Unknown keystroke, ignore
            reset();
-           return events;
+           return;
 
        case CSI_ENTRY:
            // Numbers - parameter values
            if ((ch >= '0') && (ch <= '9')) {
                params.set(paramI, params.get(paramI) + ch);
                state = ParseState.CSI_PARAM;
-               return events;
+               return;
            }
            // Parameter separator
            if (ch == ';') {
                paramI++;
                params.set(paramI, "");
-               return events;
+               return;
            }
 
            if ((ch >= 0x30) && (ch <= 0x7E)) {
@@ -874,7 +966,7 @@ public class ECMA48Terminal {
                    }
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'B':
                    // Down
                    keypress = new TKeypressEvent();
@@ -893,7 +985,7 @@ public class ECMA48Terminal {
                    }
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'C':
                    // Right
                    keypress = new TKeypressEvent();
@@ -912,7 +1004,7 @@ public class ECMA48Terminal {
                    }
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'D':
                    // Left
                    keypress = new TKeypressEvent();
@@ -931,7 +1023,7 @@ public class ECMA48Terminal {
                    }
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'H':
                    // Home
                    keypress = new TKeypressEvent();
@@ -939,7 +1031,7 @@ public class ECMA48Terminal {
                    keypress.key.fnKey = TKeypress.HOME;
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'F':
                    // End
                    keypress = new TKeypressEvent();
@@ -947,7 +1039,7 @@ public class ECMA48Terminal {
                    keypress.key.fnKey = TKeypress.END;
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'Z':
                    // CBT - Cursor backward X tab stops (default 1)
                    keypress = new TKeypressEvent();
@@ -955,11 +1047,11 @@ public class ECMA48Terminal {
                    keypress.key.fnKey = TKeypress.BTAB;
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'M':
                    // Mouse position
                    state = ParseState.MOUSE;
-                   return events;
+                   return;
                default:
                    break;
                }
@@ -967,26 +1059,26 @@ public class ECMA48Terminal {
 
            // Unknown keystroke, ignore
            reset();
-           return events;
+           return;
 
        case CSI_PARAM:
            // Numbers - parameter values
            if ((ch >= '0') && (ch <= '9')) {
                params.set(paramI, params.get(paramI) + ch);
                state = ParseState.CSI_PARAM;
-               return events;
+               return;
            }
            // Parameter separator
            if (ch == ';') {
                paramI++;
                params.set(paramI, "");
-               return events;
+               return;
            }
 
            if (ch == '~') {
                events.add(csiFnKey());
                reset();
-               return events;
+               return;
            }
 
            if ((ch >= 0x30) && (ch <= 0x7E)) {
@@ -1009,7 +1101,7 @@ public class ECMA48Terminal {
                    }
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'B':
                    // Down
                    keypress = new TKeypressEvent();
@@ -1028,7 +1120,7 @@ public class ECMA48Terminal {
                    }
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'C':
                    // Right
                    keypress = new TKeypressEvent();
@@ -1047,7 +1139,7 @@ public class ECMA48Terminal {
                    }
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                case 'D':
                    // Left
                    keypress = new TKeypressEvent();
@@ -1066,7 +1158,7 @@ public class ECMA48Terminal {
                    }
                    events.add(keypress);
                    reset();
-                   return events;
+                   return;
                default:
                    break;
                }
@@ -1074,7 +1166,7 @@ public class ECMA48Terminal {
 
            // Unknown keystroke, ignore
            reset();
-           return events;
+           return;
 
        case MOUSE:
            params.set(0, params.get(paramI) + ch);
@@ -1083,26 +1175,23 @@ public class ECMA48Terminal {
                events.add(parseMouse());
                reset();
            }
-           return events;
+           return;
 
        default:
            break;
        }
 
        // This "should" be impossible to reach
-       return events;
+       return;
     }
 
     /**
-     * Tell (u)xterm that we want alt- keystrokes to send escape +
-     * character rather than set the 8th bit.  Anyone who wants UTF8
-     * should want this enabled.
+     * Tell (u)xterm that we want alt- keystrokes to send escape + character
+     * rather than set the 8th bit.  Anyone who wants UTF8 should want this
+     * enabled.
      *
-     * Params:
-     *    on = if true, enable metaSendsEscape
-     *
-     * Returns:
-     *    the string to emit to xterm
+     * @param on if true, enable metaSendsEscape
+     * @return the string to emit to xterm
      */
     static public String xtermMetaSendsEscape(boolean on) {
        if (on) {
@@ -1112,15 +1201,13 @@ public class ECMA48Terminal {
     }
 
     /**
-     * Convert a list of SGR parameters into a full escape sequence.
-     * This also eliminates a trailing ';' which would otherwise reset
-     * everything to white-on-black not-bold.
-     *
-     * Params:
-     *    str = string of parameters, e.g. "31;1;"
+     * Convert a list of SGR parameters into a full escape sequence.  This
+     * also eliminates a trailing ';' which would otherwise reset everything
+     * to white-on-black not-bold.
      *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;1m"
+     * @param str string of parameters, e.g. "31;1;"
+     * @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) {
@@ -1135,12 +1222,10 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence for a single color change.
      *
-     * Params:
-     *    color = one of the Color.WHITE, Color.BLUE, etc. constants
-     *    foreground = if true, this is a foreground color
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[42m"
+     * @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);
@@ -1149,14 +1234,12 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence for a single color change.
      *
-     * Params:
-     *    color = one of the Color.WHITE, Color.BLUE, etc. constants
-     *    foreground = if true, this is a foreground color
-     *    header = if true, make the full header, otherwise just emit
-     *    the color parameter e.g. "42;"
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[42m"
+     * @param color one of the Color.WHITE, Color.BLUE, etc. constants
+     * @param foreground if true, this is a foreground color
+     * @param header if true, make the full header, otherwise just emit the
+     * color parameter e.g. "42;"
+     * @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) {
@@ -1181,12 +1264,10 @@ public class ECMA48Terminal {
      * Create a SGR parameter sequence for both foreground and
      * background color change.
      *
-     * Params:
-     *    foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
-     *    backColor = one of the Color.WHITE, Color.BLUE, etc. constants
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;42m"
+     * @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);
@@ -1196,14 +1277,12 @@ public class ECMA48Terminal {
      * Create a SGR parameter sequence for both foreground and
      * background color change.
      *
-     * Params:
-     *    foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
-     *    backColor = one of the Color.WHITE, Color.BLUE, etc. constants
-     *    header = if true, make the full header, otherwise just emit
-     *    the color parameter e.g. "31;42;"
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[31;42m"
+     * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
+     * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
+     * @param header if true, make the full header, otherwise just emit the
+     * color parameter e.g. "31;42;"
+     * @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) {
@@ -1224,19 +1303,17 @@ public class ECMA48Terminal {
 
     /**
      * 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.
+     * several attributes.  This sequence first resets all attributes to
+     * default, then sets attributes as per the parameters.
      *
-     * Params:
-     *    foreColor = one of the Color.WHITE, Color.BLUE, etc. constants
-     *    backColor = one of the Color.WHITE, Color.BLUE, etc. constants
-     *    bold = if true, set bold
-     *    reverse = if true, set reverse
-     *    blink = if true, set blink
-     *    underline = if true, set underline
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0;1;31;42m"
+     * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
+     * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
+     * @param bold if true, set bold
+     * @param reverse if true, set reverse
+     * @param blink if true, set blink
+     * @param underline if true, set underline
+     * @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) {
@@ -1290,11 +1367,9 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence for enabling reverse color.
      *
-     * Params:
-     *    on = if true, turn on reverse
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[7m"
+     * @param on if true, turn on reverse
+     * @return the string to emit to an ANSI / ECMA-style terminal,
+     * e.g. "\033[7m"
      */
     static public String reverse(boolean on) {
        if (on) {
@@ -1306,8 +1381,8 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence to reset to defaults.
      *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0m"
+     * @return the string to emit to an ANSI / ECMA-style terminal,
+     * e.g. "\033[0m"
      */
     static public String normal() {
        return normal(true);
@@ -1316,12 +1391,10 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence to reset to defaults.
      *
-     * Params:
-     *    header = if true, make the full header, otherwise just emit
-     *    the bare parameter e.g. "0;"
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[0m"
+     * @param header if true, make the full header, otherwise just emit the
+     * bare parameter e.g. "0;"
+     * @return the string to emit to an ANSI / ECMA-style terminal,
+     * e.g. "\033[0m"
      */
     static public String normal(boolean header) {
        if (header) {
@@ -1333,11 +1406,9 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence for enabling boldface.
      *
-     * Params:
-     *    on = if true, turn on bold
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[1m"
+     * @param on if true, turn on bold
+     * @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);
@@ -1346,13 +1417,11 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence for enabling boldface.
      *
-     * Params:
-     *    on = if true, turn on bold
-     *    header = if true, make the full header, otherwise just emit
-     *    the bare parameter e.g. "1;"
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[1m"
+     * @param on if true, turn on bold
+     * @param header if true, make the full header, otherwise just emit the
+     * bare parameter e.g. "1;"
+     * @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) {
@@ -1370,11 +1439,9 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence for enabling blinking text.
      *
-     * Params:
-     *    on = if true, turn on blink
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[5m"
+     * @param on if true, turn on blink
+     * @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);
@@ -1383,13 +1450,11 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence for enabling blinking text.
      *
-     * Params:
-     *    on = if true, turn on blink
-     *    header = if true, make the full header, otherwise just emit
-     *    the bare parameter e.g. "5;"
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[5m"
+     * @param on if true, turn on blink
+     * @param header if true, make the full header, otherwise just emit the
+     * bare parameter e.g. "5;"
+     * @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) {
@@ -1405,14 +1470,12 @@ public class ECMA48Terminal {
     }
 
     /**
-     * Create a SGR parameter sequence for enabling underline /
-     * underscored text.
-     *
-     * Params:
-     *    on = if true, turn on underline
+     * Create a SGR parameter sequence for enabling underline / underscored
+     * text.
      *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal, e.g. "\033[4m"
+     * @param on if true, turn on underline
+     * @return the string to emit to an ANSI / ECMA-style terminal,
+     * e.g. "\033[4m"
      */
     static public String underline(boolean on) {
        if (on) {
@@ -1424,11 +1487,8 @@ public class ECMA48Terminal {
     /**
      * Create a SGR parameter sequence for enabling the visible cursor.
      *
-     * Params:
-     *    on = if true, turn on cursor
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal
+     * @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)) {
@@ -1446,8 +1506,7 @@ public class ECMA48Terminal {
      * Clear the entire screen.  Because some terminals use back-color-erase,
      * set the color to white-on-black beforehand.
      *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal
+     * @return the string to emit to an ANSI / ECMA-style terminal
      */
     static public String clearAll() {
        return "\033[0;37;40m\033[2J";
@@ -1458,8 +1517,7 @@ public class ECMA48Terminal {
      * Because some terminals use back-color-erase, set the color to
      * white-on-black beforehand.
      *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal
+     * @return the string to emit to an ANSI / ECMA-style terminal
      */
     static public String clearRemainingLine() {
        return "\033[0;37;40m\033[K";
@@ -1469,8 +1527,7 @@ public class ECMA48Terminal {
      * Clear the line up the cursor (inclusive).  Because some terminals use
      * back-color-erase, set the color to white-on-black beforehand.
      *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal
+     * @return the string to emit to an ANSI / ECMA-style terminal
      */
     static public String clearPreceedingLine() {
        return "\033[0;37;40m\033[1K";
@@ -1480,8 +1537,7 @@ public class ECMA48Terminal {
      * Clear the line.  Because some terminals use back-color-erase, set the
      * color to white-on-black beforehand.
      *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal
+     * @return the string to emit to an ANSI / ECMA-style terminal
      */
     static public String clearLine() {
        return "\033[0;37;40m\033[2K";
@@ -1490,8 +1546,7 @@ public class ECMA48Terminal {
     /**
      * Move the cursor to the top-left corner.
      *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal
+     * @return the string to emit to an ANSI / ECMA-style terminal
      */
     static public String home() {
        return "\033[H";
@@ -1500,29 +1555,25 @@ public class ECMA48Terminal {
     /**
      * Move the cursor to (x, y).
      *
-     * Params:
-     *    x = column coordinate.  0 is the left-most column.
-     *    y = row coordinate.  0 is the top-most row.
-     *
-     * Returns:
-     *    the string to emit to an ANSI / ECMA-style terminal
+     * @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);
     }
 
     /**
-     * Tell (u)xterm that we want to receive mouse events based on
-     * "Any event tracking" and UTF-8 coordinates.  See
+     * Tell (u)xterm that we want to receive mouse events based on "Any event
+     * tracking" and UTF-8 coordinates.  See
      * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
      *
-     * Finally, this sets the alternate screen buffer.
-     *
-     * Params:
-     *    on = if true, enable mouse report
+     * Note that this also sets the alternate/primary screen buffer.
      *
-     * Returns:
-     *    the string to emit to xterm
+     * @param on If true, enable mouse report and use the alternate screen
+     * buffer.  If false disable mouse reporting and use the primary screen
+     * buffer.
+     * @return the string to emit to xterm
      */
     static public String mouse(boolean on) {
        if (on) {
@@ -1531,4 +1582,63 @@ public class ECMA48Terminal {
        return "\033[?1003;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 == false) && (stopReaderThread == false)) {
+           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, n);
+                   // 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);
+                           if (events.size() > 0) {
+                               // Add to the queue for the backend thread to
+                               // be able to obtain.
+                               synchronized (eventQueue) {
+                                   eventQueue.addAll(events);
+                               }
+                               // Now wake up the backend
+                               synchronized (this) {
+                                   this.notifyAll();
+                               }
+                               events.clear();
+                           }
+                       }
+                   }
+               } else {
+                   // Wait 5 millis for more data
+                   Thread.sleep(5);
+               }
+               // 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();
+    }
+
 }