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;
* 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.
/**
* 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
*/
private boolean brokenTerminalUTFMouse = false;
+ /**
+ * Get the output writer.
+ *
+ * @return the Writer
+ */
+ public PrintWriter getOutput() {
+ return output;
+ }
+
/**
* Call 'stty cooked' to set cooked mode.
*/
*/
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);
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) {
// 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());
}
/**
*/
private void reset() {
state = ParseState.GROUND;
+ params = new ArrayList<String>();
paramI = 0;
params.clear();
params.add("");
// 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:
}
/**
- * 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.
+ * @return list of new events (which may be empty)
+ */
+ 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;
+ }
+
+ /**
+ * Parses the next character of input to see if an InputEvent is fully
+ * here.
*
- * Returns:
- * list of new events (which may be empty)
+ * @param ch Unicode code point
+ * @return list of new events (which may be empty)
*/
public List<TInputEvent> getEvents(char ch) {
return getEvents(ch, false);
* 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 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;
reset();
}
}
+ */
if (noChar == true) {
int newWidth = session.getWindowWidth();
}
/**
- * 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) {
}
/**
- * 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) {
/**
* 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);
/**
* 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) {
* 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);
* 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) {
/**
* 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.
- *
- * 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
+ * several attributes. This sequence first resets all attributes to
+ * default, then sets attributes as per the parameters.
*
- * 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) {
/**
* 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) {
/**
* 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);
/**
* 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) {
/**
* 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);
/**
* 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) {
/**
* 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);
/**
* 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) {
}
/**
- * 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) {
/**
* 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)) {
* 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";
* 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";
* 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";
* 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";
/**
* 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";
/**
* 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) {
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();
+ }
+
}