HELLO WORLD...
authorKevin Lamonte <kevin.lamonte@gmail.com>
Tue, 10 Mar 2015 20:37:50 +0000 (16:37 -0400)
committerKevin Lamonte <kevin.lamonte@gmail.com>
Tue, 10 Mar 2015 20:37:50 +0000 (16:37 -0400)
demos/Demo1.java
src/jexer/TApplication.java
src/jexer/TCommand.java
src/jexer/TKeypress.java
src/jexer/bits/Color.java
src/jexer/bits/GraphicsChars.java
src/jexer/io/ECMA48Terminal.java
src/jexer/session/TTYSessionInfo.java

index ada690d6703a14724eea6fd5fec9bda237492258..62fce24c3022bb46560a067d4f56bba97319cd8d 100644 (file)
@@ -42,7 +42,9 @@ class DemoApplication extends TApplication {
     /**
      * Public constructor
      */
-    public DemoApplication() {
+    public DemoApplication() throws Exception {
+       super(null, null);
+       /*
        try {
            ColorTheme theme = new ColorTheme();
            TTYSessionInfo tty = new TTYSessionInfo();
@@ -51,6 +53,7 @@ class DemoApplication extends TApplication {
        } catch (Exception e) {
            e.printStackTrace();
        }
+        */
     }
 }
 
@@ -64,8 +67,12 @@ public class Demo1 {
      * @param  args Command line arguments
      */
     public static void main(String [] args) {
-       DemoApplication app = new DemoApplication();
-       app.run();
+       try {
+           DemoApplication app = new DemoApplication();
+           app.run();
+       } catch (Exception e) {
+           e.printStackTrace();
+       }
     }
 
 }
index 3f748036ff670ab45339b09689bc6f22da02e7d8..5df59d9d4d9def15bfca59c6dd7cd564acc50f85 100644 (file)
  */
 package jexer;
 
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.LinkedList;
+import java.util.List;
+
+import jexer.bits.CellAttributes;
+import jexer.bits.ColorTheme;
+import jexer.bits.GraphicsChars;
+import jexer.event.TCommandEvent;
+import jexer.event.TInputEvent;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+import jexer.backend.Backend;
+import jexer.backend.ECMA48Backend;
+import static jexer.TCommand.*;
+import static jexer.TKeypress.*;
+
 /**
  * TApplication sets up a full Text User Interface application.
  */
 public class TApplication {
 
     /**
-     * Run this application until it exits, using stdin and stdout
+     * Access to the physical screen, keyboard, and mouse.
+     */
+    public Backend backend;
+
+    /**
+     * Actual mouse coordinate X
+     */
+    private int mouseX;
+
+    /**
+     * Actual mouse coordinate Y
+     */
+    private int mouseY;
+
+    /**
+     * Event queue that will be drained by either primary or secondary Fiber
+     */
+    private List<TInputEvent> eventQueue;
+
+    /**
+     * Windows and widgets pull colors from this ColorTheme.
+     */
+    public ColorTheme theme;
+
+    /**
+     * When true, exit the application.
+     */
+    public boolean quit = false;
+
+    /**
+     * When true, repaint the entire screen.
+     */
+    public boolean repaint = true;
+
+    /**
+     * When true, just flush updates from the screen.
      */
-    final public void run() {
+    public boolean flush = false;
+
+    /**
+     * Y coordinate of the top edge of the desktop.
+     */
+    static public final int desktopTop = 1;
+
+    /**
+     * Y coordinate of the bottom edge of the desktop.
+     */
+    public int desktopBottom;
+
+    /**
+     * Public constructor.
+     *
+     * @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
+     * mode.  input is always converted to a Reader with UTF-8 encoding.
+     * @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.
+     */
+    public TApplication(InputStream input, OutputStream output) throws UnsupportedEncodingException {
+
+       backend       = new ECMA48Backend(input, output);
+       theme         = new ColorTheme();
+       desktopBottom = backend.screen.getHeight() - 1;
+       eventQueue    = new LinkedList<TInputEvent>();
+    }
+
+    /**
+     * Invert the cell at the mouse pointer position.
+     */
+    private void drawMouse() {
+       CellAttributes attr = backend.screen.getAttrXY(mouseX, mouseY);
+       attr.foreColor = attr.foreColor.invert();
+       attr.backColor = attr.backColor.invert();
+       backend.screen.putAttrXY(mouseX, mouseY, attr, false);
+       flush = true;
 
        /*
-       while (quit == false) {
+       if (windows.length == 0) {
+           repaint = true;
+       }
+        */
+       // TODO: remove this repaint after the above if (windows.length == 0)
+       // can be used again.
+       repaint = true;
+    }
+
+    /**
+     * Draw everything.
+     */
+    final public void drawAll() {
+       if ((flush) && (!repaint)) {
+           backend.flushScreen();
+           flush = false;
+           return;
+       }
+
+       if (!repaint) {
+           return;
+       }
+
+       // If true, the cursor is not visible
+       boolean cursor = false;
+
+       // Start with a clean screen
+       backend.screen.clear();
+
+       // Draw the background
+       CellAttributes background = theme.getColor("tapplication.background");
+       backend.screen.putAll(GraphicsChars.HATCH, background);
+
+       /*
+       // Draw each window in reverse Z order
+       TWindow [] sorted = windows.dup;
+       sorted.sort.reverse;
+       foreach (w; sorted) {
+           w.drawChildren();
+       }
+
+       // Draw the blank menubar line - reset the screen clipping first so
+       // it won't trim it out.
+       backend.screen.resetClipping();
+       backend.screen.hLineXY(0, 0, backend.screen.getWidth(), ' ',
+           theme.getColor("tmenu"));
+       // Now draw the menus.
+       int x = 1;
+       foreach (m; menus) {
+           CellAttributes menuColor;
+           CellAttributes menuMnemonicColor;
+           if (m.active) {
+               menuColor = theme.getColor("tmenu.highlighted");
+               menuMnemonicColor = theme.getColor("tmenu.mnemonic.highlighted");
+           } else {
+               menuColor = theme.getColor("tmenu");
+               menuMnemonicColor = theme.getColor("tmenu.mnemonic");
+           }
+           // Draw the menu title
+           backend.screen.hLineXY(x, 0, cast(int)m.title.length + 2, ' ',
+               menuColor);
+           backend.screen.putStrXY(x + 1, 0, m.title, menuColor);
+           // Draw the highlight character
+           backend.screen.putCharXY(x + 1 + m.mnemonic.shortcutIdx, 0,
+               m.mnemonic.shortcut, menuMnemonicColor);
+
+           if (m.active) {
+               m.drawChildren();
+               // Reset the screen clipping so we can draw the next title.
+               backend.screen.resetClipping();
+           }
+           x += m.title.length + 2;
+       }
+
+       foreach (m; subMenus) {
+           // Reset the screen clipping so we can draw the next sub-menu.
+           backend.screen.resetClipping();
+           m.drawChildren();
+       }
+        */
+
+       // Draw the mouse pointer
+       drawMouse();
 
-           // Timeout is in milliseconds, so default timeout after 1
-           // second of inactivity.
-           uint timeout = getSleepTime(1000);
+       /*
+       // Place the cursor if it is visible
+       TWidget activeWidget = null;
+       if (sorted.length > 0) {
+           activeWidget = sorted[$ - 1].getActiveChild();
+           if (activeWidget.hasCursor) {
+               backend.screen.putCursor(true, activeWidget.getCursorAbsoluteX(),
+                   activeWidget.getCursorAbsoluteY());
+               cursor = true;
+           }
+       }
+
+       // Kill the cursor
+       if (cursor == false) {
+           backend.screen.hideCursor();
+       }
+        */
+
+       // Flush the screen contents
+       backend.flushScreen();
+
+       repaint = false;
+       flush = false;
+    }
+
+    /**
+     * Run this application until it exits, using stdin and stdout
+     */
+    public final void run() {
+
+       while (quit == false) {
+           // Timeout is in milliseconds, so default timeout after 1 second
+           // of inactivity.
+           int timeout = getSleepTime(1000);
            // std.stdio.stderr.writefln("poll() timeout: %d", timeout);
 
-           if (eventQueue.length > 0) {
+           if (eventQueue.size() > 0) {
                // Do not wait if there are definitely events waiting to be
                // processed or a screen redraw to do.
                timeout = 0;
            }
 
            // Pull any pending input events
-           TInputEvent [] events = backend.getEvents(timeout);
+           List<TInputEvent> events = backend.getEvents(timeout);
            metaHandleEvents(events);
 
            // Process timers and call doIdle()'s
@@ -67,6 +273,8 @@ public class TApplication {
            drawAll();
        }
 
+       /*
+
        // Shutdown the fibers
        eventQueue.length = 0;
        if (secondaryEventFiber !is null) {
@@ -82,10 +290,149 @@ public class TApplication {
            // Wake up the primary handler so that it can exit.
            primaryEventFiber.call();
        }
+        */
 
        backend.shutdown();
+    }
+
+    /**
+     * Peek at certain application-level events, add to eventQueue, and wake
+     * up the consuming Fiber.
+     *
+     * @param events the input events to consume
+     */
+    private void metaHandleEvents(List<TInputEvent> events) {
+
+       for (TInputEvent event: events) {
+
+           /*
+           System.err.printf(String.format("metaHandleEvents event: %s\n",
+                   event)); System.err.flush();
+            */
+
+           if (quit == true) {
+               // Do no more processing if the application is already trying
+               // to exit.
+               return;
+           }
+
+           // DEBUG
+           if (event instanceof TKeypressEvent) {
+               TKeypressEvent keypress = (TKeypressEvent)event;
+               if (keypress.key.equals(kbAltX)) {
+                   quit = true;
+                   return;
+               }
+           }
+           // DEBUG
+
+           // Special application-wide events -------------------------------
+
+           // Abort everything
+           if (event instanceof TCommandEvent) {
+               TCommandEvent command = (TCommandEvent)event;
+               if (command.cmd.equals(cmAbort)) {
+                   quit = true;
+                   return;
+               }
+           }
+
+           // Screen resize
+           if (event instanceof TResizeEvent) {
+               TResizeEvent resize = (TResizeEvent)event;
+               backend.screen.setDimensions(resize.width, resize.height);
+               desktopBottom = backend.screen.getHeight() - 1;
+               repaint = true;
+               mouseX = 0;
+               mouseY = 0;
+               continue;
+           }
+
+           // Peek at the mouse position
+           if (event instanceof TMouseEvent) {
+               TMouseEvent mouse = (TMouseEvent)event;
+               if ((mouseX != mouse.x) || (mouseY != mouse.y)) {
+                   mouseX = mouse.x;
+                   mouseY = mouse.y;
+                   drawMouse();
+               }
+           }
+
+           /*
+
+           // Put into the main queue
+           addEvent(event);
+
+           // Have one of the two consumer Fibers peel the events off
+           // the queue.
+           if (secondaryEventFiber !is null) {
+               assert(secondaryEventFiber.state == Fiber.State.HOLD);
+
+               // Wake up the secondary handler for these events
+               secondaryEventFiber.call();
+           } else {
+               assert(primaryEventFiber.state == Fiber.State.HOLD);
+
+               // Wake up the primary handler for these events
+               primaryEventFiber.call();
+           }
+            */
+
+       } // for (TInputEvent event: events)
+
+    }
+
+    /**
+     * Do stuff when there is no user input.
+     */
+    private void doIdle() {
+       /*
+       // Now run any timers that have timed out
+       auto now = Clock.currTime;
+       TTimer [] keepTimers;
+       foreach (t; timers) {
+           if (t.nextTick < now) {
+               t.tick();
+               if (t.recurring == true) {
+                   keepTimers ~= t;
+               }
+           } else {
+               keepTimers ~= t;
+           }
+       }
+       timers = keepTimers;
+
+       // Call onIdle's
+       foreach (w; windows) {
+           w.onIdle();
+       }
         */
+    }
 
-       System.out.println("Hello");
+    /**
+     * Get the amount of time I can sleep before missing a Timer tick.
+     *
+     * @param timeout = initial (maximum) timeout
+     * @return number of milliseconds between now and the next timer event
+     */
+    protected int getSleepTime(int timeout) {
+       /*
+       auto now = Clock.currTime;
+       auto sleepTime = dur!("msecs")(timeout);
+       foreach (t; timers) {
+           if (t.nextTick < now) {
+               return 0;
+           }
+           if ((t.nextTick > now) &&
+               ((t.nextTick - now) < sleepTime)
+           ) {
+               sleepTime = t.nextTick - now;
+           }
+       }
+       assert(sleepTime.total!("msecs")() >= 0);
+       return cast(uint)sleepTime.total!("msecs")();
+        */
+       return 0;
     }
+
 }
index 0a3890352ebfa0027817df610c10e91f11d5b62a..ce19e2a06233f0e9097aded7b11fdc4ada0b9303 100644 (file)
@@ -144,6 +144,19 @@ public class TCommand {
        return String.format("%s", type);
     }
 
+    /**
+     * Comparison.  All fields must match to return true.
+     */
+    @Override
+    public boolean equals(Object rhs) {
+       if (!(rhs instanceof TCommand)) {
+           return false;
+       }
+
+       TCommand that = (TCommand)rhs;
+       return (type == that.type);
+    }
+
     static public final TCommand cmAbort      = new TCommand(TCommand.Type.ABORT);
     static public final TCommand cmExit       = new TCommand(TCommand.Type.EXIT);
     static public final TCommand cmQuit       = new TCommand(TCommand.Type.EXIT);
index 4f5f0afcd74007df6815fe8f64a269f04019e7d6..c4fd8b45b181dd9eb756ec5378864d1d1b38c3ca 100644 (file)
@@ -225,6 +225,24 @@ public class TKeypress {
        this.shift = shift;
     }
 
+    /**
+     * Comparison.  All fields must match to return true.
+     */
+    @Override
+    public boolean equals(Object rhs) {
+       if (!(rhs instanceof TKeypress)) {
+           return false;
+       }
+
+       TKeypress that = (TKeypress)rhs;
+       return ((isKey == that.isKey) &&
+           (fnKey == that.fnKey) &&
+           (ch == that.ch) &&
+           (alt == that.alt) &&
+           (ctrl == that.ctrl) &&
+           (shift == that.shift));
+    }
+
     /**
      * Make human-readable description of this Keystroke.
      */
index 88cc5843cd07163b8f37a93213f26641f4d47bc4..dd3b93de18c72dd2005e1a5bd01e45f02aca7f77 100644 (file)
@@ -101,12 +101,10 @@ public class Color {
 
     /**
      * Invert a color in the same way as (CGA/VGA color XOR 0x7).
-     *
-     * @param color color to change
      * @return the inverted color
      */
-    static public Color invert(Color color) {
-       switch (color.value) {
+    public Color invert() {
+       switch (value) {
        case black:
            return Color.WHITE;
        case white:
@@ -125,6 +123,19 @@ public class Color {
            return Color.BLUE;
        }
        throw new IllegalArgumentException("Invalid Color value: " +
-           color.value);
+           value);
+    }
+
+    /**
+     * Comparison.  All fields must match to return true.
+     */
+    @Override
+    public boolean equals(Object rhs) {
+       if (!(rhs instanceof Color)) {
+           return false;
+       }
+
+       Color that = (Color)rhs;
+       return (value == that.value);
     }
 }
index 12e6343487be377ebf59b820f2e579bbf004021c..1929631d4bb06b249bd3313397b59ee3385324f0 100644 (file)
  */
 package jexer.bits;
 
-// Commonly used ASCII characters
-
-/*
-public static final byte C_NUL = 0x00;         // NUL
-public static final byte C_SOH = 0x01;         // SOH
-public static final byte C_STX = 0x02;         // STX
-public static final byte C_EOT = 0x04;         // EOT
-public static final byte C_ACK = 0x06;         // ACK
-public static final byte C_LF  = 0x0A;         // Line feed '\n'
-public static final byte C_CR  = 0x0D;         // Carriage return '\r'
-public static final byte C_XON = 0x11;         // XON, also known as DC1
-public static final byte C_XOFF        = 0x13;         // XOFF, also known as DC3
-public static final byte C_NAK = 0x15;         // NAK
-public static final byte C_CAN = 0x18;         // CAN
-public static final byte C_SUB = 0x1A;         // SUB
-public static final byte C_ESC = 0x1B;         // ESC
- */
-
 /**
  * Collection of special characters used by the windowing system.
  */
@@ -58,7 +40,7 @@ public class GraphicsChars {
     /**
      * CP437 translation map
      */
-    private static final char cp437_chars[] = {
+    static private final char cp437_chars[] = {
        
     '\u2007', '\u263A', '\u263B', '\u2665', '\u2666', '\u2663', '\u2660', '\u2022',
     '\u25D8', '\u25CB', '\u25D9', '\u2642', '\u2640', '\u266A', '\u266B', '\u263C',
@@ -96,37 +78,37 @@ public class GraphicsChars {
     };
 
 
-    public static final char HATCH                     = cp437_chars[0xB0];
-    public static final char DOUBLE_BAR                        = cp437_chars[0xCD];
-    public static final char BOX                       = cp437_chars[0xFE];
-    public static final char CHECK                     = cp437_chars[0xFB];
-    public static final char TRIPLET                   = cp437_chars[0xF0];
-    public static final char OMEGA                     = cp437_chars[0xEA];
-    public static final char PI                                = cp437_chars[0xE3];
-    public static final char UPARROW                   = cp437_chars[0x18];
-    public static final char DOWNARROW                 = cp437_chars[0x19];
-    public static final char RIGHTARROW                        = cp437_chars[0x1A];
-    public static final char LEFTARROW                 = cp437_chars[0x1B];
-    public static final char SINGLE_BAR                        = cp437_chars[0xC4];
-    public static final char BACK_ARROWHEAD            = cp437_chars[0x11];
-    public static final char LRCORNER                  = cp437_chars[0xD9];
-    public static final char URCORNER                  = cp437_chars[0xBF];
-    public static final char LLCORNER                  = cp437_chars[0xC0];
-    public static final char ULCORNER                  = cp437_chars[0xDA];
-    public static final char DEGREE                    = cp437_chars[0xF8];
-    public static final char PLUSMINUS                 = cp437_chars[0xF1];
-    public static final char WINDOW_TOP                        = cp437_chars[0xCD];
-    public static final char WINDOW_LEFT_TOP           = cp437_chars[0xD5];
-    public static final char WINDOW_RIGHT_TOP          = cp437_chars[0xB8];
-    public static final char WINDOW_SIDE               = cp437_chars[0xB3];
-    public static final char WINDOW_LEFT_BOTTOM                = cp437_chars[0xD4];
-    public static final char WINDOW_RIGHT_BOTTOM       = cp437_chars[0xBE];
-    public static final char WINDOW_LEFT_TEE           = cp437_chars[0xC6];
-    public static final char WINDOW_RIGHT_TEE          = cp437_chars[0xB5];
-    public static final char WINDOW_SIDE_DOUBLE                = cp437_chars[0xBA];
-    public static final char WINDOW_LEFT_TOP_DOUBLE    = cp437_chars[0xC9];
-    public static final char WINDOW_RIGHT_TOP_DOUBLE   = cp437_chars[0xBB];
-    public static final char WINDOW_LEFT_BOTTOM_DOUBLE = cp437_chars[0xC8];
-    public static final char WINDOW_RIGHT_BOTTOM_DOUBLE        = cp437_chars[0xBC];
+    static public final char HATCH                     = cp437_chars[0xB0];
+    static public final char DOUBLE_BAR                        = cp437_chars[0xCD];
+    static public final char BOX                       = cp437_chars[0xFE];
+    static public final char CHECK                     = cp437_chars[0xFB];
+    static public final char TRIPLET                   = cp437_chars[0xF0];
+    static public final char OMEGA                     = cp437_chars[0xEA];
+    static public final char PI                                = cp437_chars[0xE3];
+    static public final char UPARROW                   = cp437_chars[0x18];
+    static public final char DOWNARROW                 = cp437_chars[0x19];
+    static public final char RIGHTARROW                        = cp437_chars[0x1A];
+    static public final char LEFTARROW                 = cp437_chars[0x1B];
+    static public final char SINGLE_BAR                        = cp437_chars[0xC4];
+    static public final char BACK_ARROWHEAD            = cp437_chars[0x11];
+    static public final char LRCORNER                  = cp437_chars[0xD9];
+    static public final char URCORNER                  = cp437_chars[0xBF];
+    static public final char LLCORNER                  = cp437_chars[0xC0];
+    static public final char ULCORNER                  = cp437_chars[0xDA];
+    static public final char DEGREE                    = cp437_chars[0xF8];
+    static public final char PLUSMINUS                 = cp437_chars[0xF1];
+    static public final char WINDOW_TOP                        = cp437_chars[0xCD];
+    static public final char WINDOW_LEFT_TOP           = cp437_chars[0xD5];
+    static public final char WINDOW_RIGHT_TOP          = cp437_chars[0xB8];
+    static public final char WINDOW_SIDE               = cp437_chars[0xB3];
+    static public final char WINDOW_LEFT_BOTTOM                = cp437_chars[0xD4];
+    static public final char WINDOW_RIGHT_BOTTOM       = cp437_chars[0xBE];
+    static public final char WINDOW_LEFT_TEE           = cp437_chars[0xC6];
+    static public final char WINDOW_RIGHT_TEE          = cp437_chars[0xB5];
+    static public final char WINDOW_SIDE_DOUBLE                = cp437_chars[0xBA];
+    static public final char WINDOW_LEFT_TOP_DOUBLE    = cp437_chars[0xC9];
+    static public final char WINDOW_RIGHT_TOP_DOUBLE   = cp437_chars[0xBB];
+    static public final char WINDOW_LEFT_BOTTOM_DOUBLE = cp437_chars[0xC8];
+    static public final char WINDOW_RIGHT_BOTTOM_DOUBLE        = cp437_chars[0xBC];
 }
 
index 7542f8a28bd73fdd686217223eff9dc061378a17..ba745c5e9d562b614207e1d5ce314b06a06c0c90 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
@@ -189,14 +214,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);
@@ -244,17 +267,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) {
@@ -283,18 +310,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());
     }
 
     /**
@@ -309,6 +370,7 @@ public class ECMA48Terminal {
      */
     private void reset() {
        state = ParseState.GROUND;
+       params = new ArrayList<String>();
        paramI = 0;
        params.clear();
        params.add("");
@@ -329,8 +391,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:
@@ -702,6 +768,18 @@ public class ECMA48Terminal {
      */
     public List<TInputEvent> getEvents() {
        List<TInputEvent> events = new LinkedList<TInputEvent>();
+
+       synchronized(this) {
+           if (eventQueue.size() > 0) {
+               events.addAll(eventQueue);
+               eventQueue.clear();
+           }
+       }
+
+       // TEST: drop a cmAbort
+       // events.add(new jexer.event.TCommandEvent(jexer.TCommand.cmAbort));
+       // events.add(new jexer.event.TKeypressEvent(kbAltX));
+
        return events;
     }
 
@@ -731,6 +809,7 @@ public class ECMA48Terminal {
        TKeypressEvent keypress;
        Date now = new Date();
 
+       /*
        // ESCDELAY type timeout
        if (state == ParseState.ESCAPE) {
            long escDelay = now.getTime() - escapeTime;
@@ -740,6 +819,7 @@ public class ECMA48Terminal {
                reset();
            }
        }
+        */
 
        if (noChar == true) {
            int newWidth = session.getWindowWidth();
@@ -1501,4 +1581,59 @@ 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];
+
+       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];
+
+                           // System.err.printf("** READ 0x%x '%c'", ch, ch);
+                           List<TInputEvent> events = getEvents((char)ch);
+                           synchronized (this) {
+                               /*
+                               System.err.printf("adding %d events\n",
+                                   events.size());
+                                */
+                               eventQueue.addAll(events);
+                           }
+                       }
+                   }
+               } 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();
+    }
+
 }
index b65eb0d2acb40b78f8774eae265d5651d89c17ab..2236ac3e0bf904f7f474269fc46babe096f4ea19 100644 (file)
@@ -116,8 +116,6 @@ public class TTYSessionInfo implements SessionInfo {
            "/bin/sh", "-c", "stty size < /dev/tty"
        };
        try {
-           System.out.println("spawn stty");
-
            Process process = Runtime.getRuntime().exec(cmd);
            BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8"));
            String line = in.readLine();