X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fio%2FECMA48Terminal.java;h=1aafa3c41619a4864d0297f1e966818e39e939da;hb=9b1afdde02c30f0d4a80ba330a4bc72384093253;hp=687d7a2e00fbc1728ce5a37a610167845336593b;hpb=8e688b9211599d240be67e5cf62dfe48520378f2;p=nikiroo-utils.git diff --git a/src/jexer/io/ECMA48Terminal.java b/src/jexer/io/ECMA48Terminal.java index 687d7a2..1aafa3c 100644 --- a/src/jexer/io/ECMA48Terminal.java +++ b/src/jexer/io/ECMA48Terminal.java @@ -46,7 +46,6 @@ import java.util.Date; import java.util.List; import java.util.LinkedList; -import jexer.TKeypress; import jexer.bits.Color; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; @@ -61,7 +60,7 @@ import static jexer.TKeypress.*; * This class reads keystrokes and mouse events and emits output to ANSI * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc. */ -public class ECMA48Terminal implements Runnable { +public final class ECMA48Terminal implements Runnable { /** * The session information. @@ -73,7 +72,7 @@ public class ECMA48Terminal implements Runnable { * * @return the SessionInfo */ - public final SessionInfo getSessionInfo() { + public SessionInfo getSessionInfo() { return sessionInfo; } @@ -98,11 +97,6 @@ public class ECMA48Terminal implements Runnable { */ private ArrayList params; - /** - * params[paramI] is being appended to. - */ - private int paramI; - /** * States in the input parser. */ @@ -113,7 +107,8 @@ public class ECMA48Terminal implements Runnable { CSI_ENTRY, CSI_PARAM, // CSI_INTERMEDIATE, - MOUSE + MOUSE, + MOUSE_SGR, } /** @@ -127,6 +122,12 @@ public class ECMA48Terminal implements Runnable { */ private long escapeTime; + /** + * The time we last checked the window size. We try not to spawn stty + * more than once per second. + */ + private long windowSizeTime; + /** * true if mouse1 was down. Used to report mouse1 on the release event. */ @@ -182,12 +183,9 @@ public class ECMA48Terminal implements Runnable { private PrintWriter output; /** - * When true, the terminal is sending non-UTF8 bytes when reporting mouse - * events. - * - * TODO: Add broken mouse detection back into the reader. + * The listening object that run() wakes up on new input. */ - private boolean brokenTerminalUTFMouse = false; + private Object listener; /** * Get the output writer. @@ -243,7 +241,7 @@ public class ECMA48Terminal implements Runnable { }; try { Process process; - if (mode == true) { + if (mode) { process = Runtime.getRuntime().exec(cmdRaw); } else { process = Runtime.getRuntime().exec(cmdCooked); @@ -278,6 +276,8 @@ public class ECMA48Terminal implements Runnable { /** * Constructor sets up state for getEvent(). * + * @param listener the object this backend needs to wake up when new + * input comes in * @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 @@ -288,7 +288,7 @@ public class ECMA48Terminal implements Runnable { * @throws UnsupportedEncodingException if an exception is thrown when * creating the InputStreamReader */ - public ECMA48Terminal(final InputStream input, + public ECMA48Terminal(final Object listener, final InputStream input, final OutputStream output) throws UnsupportedEncodingException { reset(); @@ -296,6 +296,7 @@ public class ECMA48Terminal implements Runnable { mouse2 = false; mouse3 = false; stopReaderThread = false; + this.listener = listener; if (input == null) { // inputStream = System.in; @@ -307,9 +308,9 @@ public class ECMA48Terminal implements Runnable { } this.input = new InputStreamReader(inputStream, "UTF-8"); - // TODO: include TelnetSocket from NIB and have it implement - // SessionInfo if (input instanceof SessionInfo) { + // This is a TelnetInputStream that exposes window size and + // environment variables from the telnet layer. sessionInfo = (SessionInfo) input; } if (sessionInfo == null) { @@ -331,6 +332,7 @@ public class ECMA48Terminal implements Runnable { // Enable mouse reporting and metaSendsEscape this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); + this.output.flush(); // Hang onto the window size windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, @@ -396,7 +398,6 @@ public class ECMA48Terminal implements Runnable { private void reset() { state = ParseState.GROUND; params = new ArrayList(); - paramI = 0; params.clear(); params.add(""); } @@ -626,6 +627,105 @@ public class ECMA48Terminal implements Runnable { eventMouseWheelUp, eventMouseWheelDown); } + /** + * Produce mouse events based on "Any event tracking" and SGR + * coordinates. See + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking + * + * @param release if true, this was a release ('m') + * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event + */ + private TInputEvent parseMouseSGR(final boolean release) { + // SGR extended coordinates - mode 1006 + if (params.size() < 3) { + // Invalid position, bail out. + return null; + } + int buttons = Integer.parseInt(params.get(0)); + int x = Integer.parseInt(params.get(1)) - 1; + int y = Integer.parseInt(params.get(2)) - 1; + + // Clamp X and Y to the physical screen coordinates. + if (x >= windowResize.getWidth()) { + x = windowResize.getWidth() - 1; + } + if (y >= windowResize.getHeight()) { + y = windowResize.getHeight() - 1; + } + + TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN; + boolean eventMouse1 = false; + boolean eventMouse2 = false; + boolean eventMouse3 = false; + boolean eventMouseWheelUp = false; + boolean eventMouseWheelDown = false; + + if (release) { + eventType = TMouseEvent.Type.MOUSE_UP; + } + + switch (buttons) { + case 0: + eventMouse1 = true; + break; + case 1: + eventMouse2 = true; + break; + case 2: + eventMouse3 = true; + break; + case 35: + // Motion only, no buttons down + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 32: + // Dragging with mouse1 down + eventMouse1 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 33: + // Dragging with mouse2 down + eventMouse2 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 34: + // Dragging with mouse3 down + eventMouse3 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 96: + // Dragging with mouse2 down after wheelUp + eventMouse2 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 97: + // Dragging with mouse2 down after wheelDown + eventMouse2 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 64: + eventMouseWheelUp = true; + break; + + case 65: + eventMouseWheelDown = true; + break; + + default: + // Unknown, bail out + return null; + } + return new TMouseEvent(eventType, x, y, x, y, + eventMouse1, eventMouse2, eventMouse3, + eventMouseWheelUp, eventMouseWheelDown); + } + /** * Return any events in the IO queue. * @@ -647,30 +747,34 @@ public class ECMA48Terminal implements Runnable { * * @param queue list to append new events to */ - public void getIdleEvents(final List queue) { + private void getIdleEvents(final List queue) { + Date now = new Date(); // Check for new window size - sessionInfo.queryWindowSize(); - int newWidth = sessionInfo.getWindowWidth(); - int newHeight = sessionInfo.getWindowHeight(); - if ((newWidth != windowResize.getWidth()) - || (newHeight != windowResize.getHeight()) - ) { - TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN, - newWidth, newHeight); - windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, - newWidth, newHeight); - synchronized (eventQueue) { - eventQueue.add(event); + long windowSizeDelay = now.getTime() - windowSizeTime; + if (windowSizeDelay > 1000) { + sessionInfo.queryWindowSize(); + int newWidth = sessionInfo.getWindowWidth(); + int newHeight = sessionInfo.getWindowHeight(); + if ((newWidth != windowResize.getWidth()) + || (newHeight != windowResize.getHeight()) + ) { + TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN, + newWidth, newHeight); + windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, + newWidth, newHeight); + queue.add(event); } + windowSizeTime = now.getTime(); } - synchronized (eventQueue) { - if (eventQueue.size() > 0) { - synchronized (queue) { - queue.addAll(eventQueue); - } - eventQueue.clear(); + // ESCDELAY type timeout + if (state == ParseState.ESCAPE) { + long escDelay = now.getTime() - escapeTime; + if (escDelay > 100) { + // After 0.1 seconds, assume a true escape character + queue.add(controlChar((char)0x1B, false)); + reset(); } } } @@ -699,8 +803,6 @@ public class ECMA48Terminal implements Runnable { boolean ctrl = false; boolean alt = false; boolean shift = false; - char keyCh = ch; - TKeypress key; // System.err.printf("state: %s ch %c\r\n", state, ch); @@ -789,14 +891,14 @@ public class ECMA48Terminal implements Runnable { case CSI_ENTRY: // Numbers - parameter values if ((ch >= '0') && (ch <= '9')) { - params.set(paramI, params.get(paramI) + ch); + params.set(params.size() - 1, + params.get(params.size() - 1) + ch); state = ParseState.CSI_PARAM; return; } // Parameter separator if (ch == ';') { - paramI++; - params.set(paramI, ""); + params.add(""); return; } @@ -885,6 +987,10 @@ public class ECMA48Terminal implements Runnable { // Mouse position state = ParseState.MOUSE; return; + case '<': + // Mouse position, SGR (1006) coordinates + state = ParseState.MOUSE_SGR; + return; default: break; } @@ -894,17 +1000,55 @@ public class ECMA48Terminal implements Runnable { reset(); return; + case MOUSE_SGR: + // Numbers - parameter values + if ((ch >= '0') && (ch <= '9')) { + params.set(params.size() - 1, + params.get(params.size() - 1) + ch); + return; + } + // Parameter separator + if (ch == ';') { + params.add(""); + return; + } + + switch (ch) { + case 'M': + // Generate a mouse press event + TInputEvent event = parseMouseSGR(false); + if (event != null) { + events.add(event); + } + reset(); + return; + case 'm': + // Generate a mouse release event + event = parseMouseSGR(true); + if (event != null) { + events.add(event); + } + reset(); + return; + default: + break; + } + + // Unknown keystroke, ignore + reset(); + return; + case CSI_PARAM: // Numbers - parameter values if ((ch >= '0') && (ch <= '9')) { - params.set(paramI, params.get(paramI) + ch); + params.set(params.size() - 1, + params.get(params.size() - 1) + ch); state = ParseState.CSI_PARAM; return; } // Parameter separator if (ch == ';') { - paramI++; - params.set(paramI, ""); + params.add(""); return; } @@ -990,7 +1134,7 @@ public class ECMA48Terminal implements Runnable { return; case MOUSE: - params.set(0, params.get(paramI) + ch); + params.set(0, params.get(params.size() - 1) + ch); if (params.get(0).length() == 3) { // We have enough to generate a mouse event events.add(parseMouse()); @@ -1014,7 +1158,7 @@ public class ECMA48Terminal implements Runnable { * @param on if true, enable metaSendsEscape * @return the string to emit to xterm */ - public String xtermMetaSendsEscape(final boolean on) { + private String xtermMetaSendsEscape(final boolean on) { if (on) { return "\033[?1036h\033[?1034l"; } @@ -1030,7 +1174,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[31;1m" */ - public String addHeaderSGR(String str) { + private String addHeaderSGR(String str) { if (str.length() > 0) { // Nix any trailing ';' because that resets all attributes while (str.endsWith(":")) { @@ -1041,14 +1185,15 @@ public class ECMA48Terminal implements Runnable { } /** - * Create a SGR parameter sequence for a single color change. + * Create a SGR parameter sequence for a single color change. Note + * package private access. * * @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" */ - public String color(final Color color, final boolean foreground) { + String color(final Color color, final boolean foreground) { return color(color, foreground, true); } @@ -1062,7 +1207,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[42m" */ - public String color(final Color color, final boolean foreground, + private String color(final Color color, final boolean foreground, final boolean header) { int ecmaColor = color.getValue(); @@ -1082,15 +1227,15 @@ public class ECMA48Terminal implements Runnable { } /** - * Create a SGR parameter sequence for both foreground and - * background color change. + * Create a SGR parameter sequence for both foreground and background + * color change. Note package private access. * * @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" */ - public String color(final Color foreColor, final Color backColor) { + String color(final Color foreColor, final Color backColor) { return color(foreColor, backColor, true); } @@ -1105,7 +1250,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[31;42m" */ - public String color(final Color foreColor, final Color backColor, + private String color(final Color foreColor, final Color backColor, final boolean header) { int ecmaForeColor = foreColor.getValue(); @@ -1125,7 +1270,8 @@ public class ECMA48Terminal implements Runnable { /** * 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. + * default, then sets attributes as per the parameters. Note package + * private access. * * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants @@ -1136,7 +1282,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[0;1;31;42m" */ - public String color(final Color foreColor, final Color backColor, + String color(final Color foreColor, final Color backColor, final boolean bold, final boolean reverse, final boolean blink, final boolean underline) { @@ -1193,7 +1339,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[7m" */ - public String reverse(final boolean on) { + private String reverse(final boolean on) { if (on) { return "\033[7m"; } @@ -1201,12 +1347,13 @@ public class ECMA48Terminal implements Runnable { } /** - * Create a SGR parameter sequence to reset to defaults. + * Create a SGR parameter sequence to reset to defaults. Note package + * private access. * * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[0m" */ - public String normal() { + String normal() { return normal(true); } @@ -1218,7 +1365,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[0m" */ - public String normal(final boolean header) { + private String normal(final boolean header) { if (header) { return "\033[0;37;40m"; } @@ -1232,7 +1379,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[1m" */ - public String bold(final boolean on) { + private String bold(final boolean on) { return bold(on, true); } @@ -1245,7 +1392,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[1m" */ - public String bold(final boolean on, final boolean header) { + private String bold(final boolean on, final boolean header) { if (header) { if (on) { return "\033[1m"; @@ -1265,7 +1412,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[5m" */ - public String blink(final boolean on) { + private String blink(final boolean on) { return blink(on, true); } @@ -1278,7 +1425,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[5m" */ - public String blink(final boolean on, final boolean header) { + private String blink(final boolean on, final boolean header) { if (header) { if (on) { return "\033[5m"; @@ -1299,7 +1446,7 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[4m" */ - public String underline(final boolean on) { + private String underline(final boolean on) { if (on) { return "\033[4m"; } @@ -1307,12 +1454,13 @@ public class ECMA48Terminal implements Runnable { } /** - * Create a SGR parameter sequence for enabling the visible cursor. + * Create a SGR parameter sequence for enabling the visible cursor. Note + * package private access. * * @param on if true, turn on cursor * @return the string to emit to an ANSI / ECMA-style terminal */ - public String cursor(final boolean on) { + String cursor(final boolean on) { if (on && !cursorOn) { cursorOn = true; return "\033[?25h"; @@ -1337,11 +1485,11 @@ public class ECMA48Terminal implements Runnable { /** * Clear the line from the cursor (inclusive) to the end of the screen. * Because some terminals use back-color-erase, set the color to - * white-on-black beforehand. + * white-on-black beforehand. Note package private access. * * @return the string to emit to an ANSI / ECMA-style terminal */ - public String clearRemainingLine() { + String clearRemainingLine() { return "\033[0;37;40m\033[K"; } @@ -1351,7 +1499,7 @@ public class ECMA48Terminal implements Runnable { * * @return the string to emit to an ANSI / ECMA-style terminal */ - public String clearPreceedingLine() { + private String clearPreceedingLine() { return "\033[0;37;40m\033[1K"; } @@ -1361,7 +1509,7 @@ public class ECMA48Terminal implements Runnable { * * @return the string to emit to an ANSI / ECMA-style terminal */ - public String clearLine() { + private String clearLine() { return "\033[0;37;40m\033[2K"; } @@ -1370,24 +1518,26 @@ public class ECMA48Terminal implements Runnable { * * @return the string to emit to an ANSI / ECMA-style terminal */ - public String home() { + private String home() { return "\033[H"; } /** - * Move the cursor to (x, y). + * Move the cursor to (x, y). Note package private access. * * @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 */ - public String gotoXY(final int x, final int y) { + String gotoXY(final int x, final 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 + * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we + * will end up with SGR coordinates with UTF-8 coordinates as a fallback. + * See * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking * * Note that this also sets the alternate/primary screen buffer. @@ -1397,11 +1547,11 @@ public class ECMA48Terminal implements Runnable { * buffer. * @return the string to emit to xterm */ - public String mouse(final boolean on) { + private String mouse(final boolean on) { if (on) { - return "\033[?1003;1005h\033[?1049h"; + return "\033[?1003;1005;1006h\033[?1049h"; } - return "\033[?1003;1005l\033[?1049l"; + return "\033[?1003;1006;1005l\033[?1049l"; } /** @@ -1425,7 +1575,7 @@ public class ECMA48Terminal implements Runnable { readBuffer = new char[readBuffer.length * 2]; } - int rc = input.read(readBuffer, 0, n); + int rc = input.read(readBuffer, 0, readBuffer.length); // System.err.printf("read() %d", rc); System.err.flush(); if (rc == -1) { // This is EOF @@ -1434,21 +1584,32 @@ public class ECMA48Terminal implements Runnable { 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(); + } + getIdleEvents(events); + if (events.size() > 0) { + // Add to the queue for the backend thread to + // be able to obtain. + synchronized (eventQueue) { + eventQueue.addAll(events); + } + synchronized (listener) { + listener.notifyAll(); } + events.clear(); } } } else { + getIdleEvents(events); + if (events.size() > 0) { + synchronized (eventQueue) { + eventQueue.addAll(events); + } + events.clear(); + synchronized (listener) { + listener.notifyAll(); + } + } + // Wait 10 millis for more data Thread.sleep(10); }