X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fio%2FECMA48Terminal.java;h=d7d23915d0c416562d0d00874a86643e2fcc7464;hb=9edb442b712de01d1b7af81d1d57a29c2c6e7871;hp=7542f8a28bd73fdd686217223eff9dc061378a17;hpb=05dbb28d6e8613216f43e8d0fae487c1d9c2fcd3;p=nikiroo-utils.git diff --git a/src/jexer/io/ECMA48Terminal.java b/src/jexer/io/ECMA48Terminal.java index 7542f8a..d7d2391 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 @@ -168,6 +193,17 @@ public class ECMA48Terminal { 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. */ @@ -189,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); @@ -244,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) { @@ -283,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(); + 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 +381,7 @@ public class ECMA48Terminal { */ private void reset() { state = ParseState.GROUND; + params = new ArrayList(); paramI = 0; params.clear(); params.add(""); @@ -329,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: @@ -698,35 +775,55 @@ public class ECMA48Terminal { /** * Return any events in the IO queue. * - * @return list of new events (which may be empty) + * @param queue list to append new events to */ - public List getEvents() { - List events = new LinkedList(); - return events; + public void getEvents(List queue) { + 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. + * Return any events in the IO queue due to timeout. * - * @param ch Unicode code point - * @return list of new events (which may be empty) + * @param queue list to append new events to */ - public List getEvents(char ch) { - return getEvents(ch, false); + public void getIdleEvents(List 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. * + * @param events list to append new events to * @param ch Unicode code point - * @param noChar if true, ignore ch. This is currently used to return a - * bare ESC and RESIZE events. - * @return list of new events (which may be empty) */ - public List getEvents(char ch, boolean noChar) { - List events = new LinkedList(); + private void processChar(List events, char ch) { TKeypressEvent keypress; Date now = new Date(); @@ -741,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) { @@ -765,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) { @@ -782,7 +863,7 @@ public class ECMA48Terminal { keypress.key.ch = ch; events.add(keypress); reset(); - return events; + return; } break; @@ -794,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 @@ -819,7 +900,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; case ESCAPE_INTERMEDIATE: if ((ch >= 'P') && (ch <= 'S')) { @@ -844,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)) { @@ -885,7 +966,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; case 'B': // Down keypress = new TKeypressEvent(); @@ -904,7 +985,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; case 'C': // Right keypress = new TKeypressEvent(); @@ -923,7 +1004,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; case 'D': // Left keypress = new TKeypressEvent(); @@ -942,7 +1023,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; case 'H': // Home keypress = new TKeypressEvent(); @@ -950,7 +1031,7 @@ public class ECMA48Terminal { keypress.key.fnKey = TKeypress.HOME; events.add(keypress); reset(); - return events; + return; case 'F': // End keypress = new TKeypressEvent(); @@ -958,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(); @@ -966,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; } @@ -978,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)) { @@ -1020,7 +1101,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; case 'B': // Down keypress = new TKeypressEvent(); @@ -1039,7 +1120,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; case 'C': // Right keypress = new TKeypressEvent(); @@ -1058,7 +1139,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; case 'D': // Left keypress = new TKeypressEvent(); @@ -1077,7 +1158,7 @@ public class ECMA48Terminal { } events.add(keypress); reset(); - return events; + return; default: break; } @@ -1085,7 +1166,7 @@ public class ECMA48Terminal { // Unknown keystroke, ignore reset(); - return events; + return; case MOUSE: params.set(0, params.get(paramI) + ch); @@ -1094,14 +1175,14 @@ public class ECMA48Terminal { events.add(parseMouse()); reset(); } - return events; + return; default: break; } // This "should" be impossible to reach - return events; + return; } /** @@ -1501,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 events = new LinkedList(); + + 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(); + } + }