X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fio%2FECMA48Terminal.java;h=ba745c5e9d562b614207e1d5ce314b06a06c0c90;hb=4328bb42c10743287dad5cf045f059ad109eb540;hp=f6ae12fdf30916c047d021e7f27d642d4ebc7817;hpb=b158962153f6f17e458a9846c4296ecd1644221b;p=nikiroo-utils.git diff --git a/src/jexer/io/ECMA48Terminal.java b/src/jexer/io/ECMA48Terminal.java index f6ae12f..ba745c5 100644 --- a/src/jexer/io/ECMA48Terminal.java +++ b/src/jexer/io/ECMA48Terminal.java @@ -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 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,15 @@ public class ECMA48Terminal { */ private boolean brokenTerminalUTFMouse = false; + /** + * Get the output writer. + * + * @return the Writer + */ + public PrintWriter getOutput() { + return output; + } + /** * Call 'stty cooked' to set cooked mode. */ @@ -180,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); @@ -235,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) { @@ -274,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(); + 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 +370,7 @@ public class ECMA48Terminal { */ private void reset() { state = ParseState.GROUND; + params = new ArrayList(); paramI = 0; params.clear(); params.add(""); @@ -320,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: @@ -687,16 +762,33 @@ 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. + * @return list of new events (which may be empty) + */ + public List getEvents() { + List events = new LinkedList(); + + 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 getEvents(char ch) { return getEvents(ch, false); @@ -706,13 +798,10 @@ public class ECMA48Terminal { * 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 getEvents(char ch, boolean noChar) { List events = new LinkedList(); @@ -720,6 +809,7 @@ public class ECMA48Terminal { TKeypressEvent keypress; Date now = new Date(); + /* // ESCDELAY type timeout if (state == ParseState.ESCAPE) { long escDelay = now.getTime() - escapeTime; @@ -729,6 +819,7 @@ public class ECMA48Terminal { reset(); } } + */ if (noChar == true) { int newWidth = session.getWindowWidth(); @@ -1094,15 +1185,12 @@ public class ECMA48Terminal { } /** - * 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 +1200,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 +1221,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 +1233,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 +1263,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 +1276,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 +1302,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. - * - * 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) { @@ -1290,11 +1366,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 +1380,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 +1390,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 +1405,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 +1416,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 +1438,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 +1449,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 +1469,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 +1486,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 +1505,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 +1516,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 +1526,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 +1536,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 +1545,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 +1554,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 +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 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(); + } + }