X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fio%2FECMA48Terminal.java;h=1aafa3c41619a4864d0297f1e966818e39e939da;hb=9b1afdde02c30f0d4a80ba330a4bc72384093253;hp=ba745c5e9d562b614207e1d5ce314b06a06c0c90;hpb=4328bb42c10743287dad5cf045f059ad109eb540;p=nikiroo-utils.git diff --git a/src/jexer/io/ECMA48Terminal.java b/src/jexer/io/ECMA48Terminal.java index ba745c5..1aafa3c 100644 --- a/src/jexer/io/ECMA48Terminal.java +++ b/src/jexer/io/ECMA48Terminal.java @@ -1,16 +1,11 @@ /** * Jexer - Java Text User Interface * - * Version: $Id$ - * - * Author: Kevin Lamonte, kevin.lamonte@gmail.com - * * License: LGPLv3 or later * - * Copyright: This module is licensed under the GNU Lesser General - * Public License Version 3. Please see the file "COPYING" in this - * directory for more information about the GNU Lesser General Public - * License Version 3. + * This module is licensed under the GNU Lesser General Public License + * Version 3. Please see the file "COPYING" in this directory for more + * information about the GNU Lesser General Public License Version 3. * * Copyright (C) 2015 Kevin Lamonte * @@ -29,6 +24,9 @@ * http://www.gnu.org/licenses/, or write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 */ package jexer.io; @@ -48,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; @@ -60,19 +57,27 @@ import jexer.session.TTYSessionInfo; import static jexer.TKeypress.*; /** - * This class has convenience methods for emitting output to ANSI - * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, - * etc. + * 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. + */ + private SessionInfo sessionInfo; /** - * The session information + * Getter for sessionInfo. + * + * @return the SessionInfo */ - public SessionInfo session; + public SessionInfo getSessionInfo() { + return sessionInfo; + } /** - * The event queue, filled up by a thread reading on input + * The event queue, filled up by a thread reading on input. */ private List eventQueue; @@ -82,7 +87,7 @@ public class ECMA48Terminal implements Runnable { private boolean stopReaderThread; /** - * The reader thread + * The reader thread. */ private Thread readerThread; @@ -93,35 +98,36 @@ public class ECMA48Terminal implements Runnable { private ArrayList params; /** - * params[paramI] is being appended to. - */ - private int paramI; - - /** - * States in the input parser + * States in the input parser. */ private enum ParseState { - GROUND, - ESCAPE, - ESCAPE_INTERMEDIATE, - CSI_ENTRY, - CSI_PARAM, - // CSI_INTERMEDIATE, - MOUSE + GROUND, + ESCAPE, + ESCAPE_INTERMEDIATE, + CSI_ENTRY, + CSI_PARAM, + // CSI_INTERMEDIATE, + MOUSE, + MOUSE_SGR, } /** - * Current parsing state + * Current parsing state. */ private ParseState state; /** - * The time we entered ESCAPE. If we get a bare escape - * without a code following it, this is used to return that bare - * escape. + * The time we entered ESCAPE. If we get a bare escape without a code + * following it, this is used to return that bare escape. */ 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. */ @@ -177,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. @@ -190,21 +193,38 @@ public class ECMA48Terminal implements Runnable { * @return the Writer */ public PrintWriter getOutput() { - return output; + 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. + * Call 'stty' to set cooked mode. + * + *

Actually executes '/bin/sh -c stty sane cooked < /dev/tty' */ private void sttyCooked() { - doStty(false); + doStty(false); } /** - * Call 'stty raw' to set raw mode. + * Call 'stty' to set raw mode. + * + *

Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip + * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten + * -parenb cs8 min 1 < /dev/tty' */ private void sttyRaw() { - doStty(true); + doStty(true); } /** @@ -212,50 +232,52 @@ public class ECMA48Terminal implements Runnable { * * @param mode if true, set raw mode, otherwise set cooked mode */ - private void doStty(boolean mode) { - String [] cmdRaw = { - "/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 sane cooked < /dev/tty" - }; - try { - Process process; - if (mode == true) { - process = Runtime.getRuntime().exec(cmdRaw); - } else { - process = Runtime.getRuntime().exec(cmdCooked); - } - BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); - String line = in.readLine(); - if ((line != null) && (line.length() > 0)) { - System.err.println("WEIRD?! Normal output from stty: " + line); - } - while (true) { - BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8")); - line = err.readLine(); - if ((line != null) && (line.length() > 0)) { - System.err.println("Error output from stty: " + line); - } - try{ - process.waitFor(); - break; - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - int rc = process.exitValue(); - if (rc != 0) { - System.err.println("stty returned error code: " + rc); - } - } catch (IOException e) { - e.printStackTrace(); - } + private void doStty(final boolean mode) { + String [] cmdRaw = { + "/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 sane cooked < /dev/tty" + }; + try { + Process process; + if (mode) { + process = Runtime.getRuntime().exec(cmdRaw); + } else { + process = Runtime.getRuntime().exec(cmdCooked); + } + BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + String line = in.readLine(); + if ((line != null) && (line.length() > 0)) { + System.err.println("WEIRD?! Normal output from stty: " + line); + } + while (true) { + BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8")); + line = err.readLine(); + if ((line != null) && (line.length() > 0)) { + System.err.println("Error output from stty: " + line); + } + try { + process.waitFor(); + break; + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + int rc = process.exitValue(); + if (rc != 0) { + System.err.println("stty returned error code: " + rc); + } + } catch (IOException e) { + e.printStackTrace(); + } } /** - * Constructor sets up state for getEvent() + * 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 @@ -263,159 +285,156 @@ public class ECMA48Terminal implements Runnable { * @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. + * @throws UnsupportedEncodingException if an exception is thrown when + * creating the InputStreamReader */ - public ECMA48Terminal(InputStream input, OutputStream output) throws UnsupportedEncodingException { - - reset(); - mouse1 = false; - mouse2 = false; - mouse3 = false; - stopReaderThread = false; - - if (input == null) { - // inputStream = System.in; - inputStream = new FileInputStream(FileDescriptor.in); - sttyRaw(); - setRawMode = true; - } else { - inputStream = input; - } - this.input = new InputStreamReader(inputStream, "UTF-8"); - - // TODO: include TelnetSocket from NIB and have it implement - // SessionInfo - if (input instanceof SessionInfo) { - session = (SessionInfo)input; - } - if (session == null) { - if (input == null) { - // Reading right off the tty - session = new TTYSessionInfo(); - } else { - session = new TSessionInfo(); - } - } - - if (output == null) { - this.output = new PrintWriter(new OutputStreamWriter(System.out, - "UTF-8")); - } else { - this.output = new PrintWriter(new OutputStreamWriter(output, - "UTF-8")); - } - - // Enable mouse reporting and metaSendsEscape - this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true)); - - // 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(); + public ECMA48Terminal(final Object listener, final InputStream input, + final OutputStream output) throws UnsupportedEncodingException { + + reset(); + mouse1 = false; + mouse2 = false; + mouse3 = false; + stopReaderThread = false; + this.listener = listener; + + if (input == null) { + // inputStream = System.in; + inputStream = new FileInputStream(FileDescriptor.in); + sttyRaw(); + setRawMode = true; + } else { + inputStream = input; + } + this.input = new InputStreamReader(inputStream, "UTF-8"); + + 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) { + if (input == null) { + // Reading right off the tty + sessionInfo = new TTYSessionInfo(); + } else { + sessionInfo = new TSessionInfo(); + } + } + + if (output == null) { + this.output = new PrintWriter(new OutputStreamWriter(System.out, + "UTF-8")); + } else { + this.output = new PrintWriter(new OutputStreamWriter(output, + "UTF-8")); + } + + // 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, + sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); + + // Spin up the input reader + eventQueue = new LinkedList(); + readerThread = new Thread(this); + readerThread.start(); } /** - * Restore terminal to normal state + * 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(); - } - } + // 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(); + } + } } /** - * Flush output + * Flush output. */ public void flush() { - output.flush(); + output.flush(); } /** - * Reset keyboard/mouse input parser + * Reset keyboard/mouse input parser. */ private void reset() { - state = ParseState.GROUND; - params = new ArrayList(); - paramI = 0; - params.clear(); - params.add(""); + state = ParseState.GROUND; + params = new ArrayList(); + params.clear(); + params.add(""); } /** * Produce a control character or one of the special ones (ENTER, TAB, - * etc.) + * etc.). * * @param ch Unicode code point - * @return one KEYPRESS event, either a control character (e.g. isKey == + * @param alt if true, set alt on the TKeypress + * @return one TKeypress event, either a control character (e.g. isKey == * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true, * fnKey == ESC) */ - private TKeypressEvent controlChar(char ch) { - TKeypressEvent event = new TKeypressEvent(); - - // System.err.printf("controlChar: %02x\n", ch); - - switch (ch) { - case 0x0D: - // Carriage return --> ENTER - event.key = kbEnter; - break; - case 0x0A: - // Linefeed --> ENTER - event.key = kbEnter; - break; - case 0x1B: - // ESC - event.key = kbEsc; - break; - case '\t': - // TAB - event.key = kbTab; - break; - default: - // Make all other control characters come back as the alphabetic - // character with the ctrl field set. So SOH would be 'A' + - // ctrl. - event.key = new TKeypress(false, 0, (char)(ch + 0x40), - false, true, false); - break; - } - return event; + private TKeypressEvent controlChar(final char ch, final boolean alt) { + // System.err.printf("controlChar: %02x\n", ch); + + switch (ch) { + case 0x0D: + // Carriage return --> ENTER + return new TKeypressEvent(kbEnter, alt, false, false); + case 0x0A: + // Linefeed --> ENTER + return new TKeypressEvent(kbEnter, alt, false, false); + case 0x1B: + // ESC + return new TKeypressEvent(kbEsc, alt, false, false); + case '\t': + // TAB + return new TKeypressEvent(kbTab, alt, false, false); + default: + // Make all other control characters come back as the alphabetic + // character with the ctrl field set. So SOH would be 'A' + + // ctrl. + return new TKeypressEvent(false, 0, (char)(ch + 0x40), + alt, true, false); + } } /** @@ -424,228 +443,72 @@ public class ECMA48Terminal implements Runnable { * @return one KEYPRESS event representing a special key */ private TInputEvent csiFnKey() { - int key = 0; - int modifier = 0; - if (params.size() > 0) { - key = Integer.parseInt(params.get(0)); - } - if (params.size() > 1) { - modifier = Integer.parseInt(params.get(1)); - } - TKeypressEvent event = new TKeypressEvent(); - - switch (modifier) { - case 0: - // No modifier - switch (key) { - case 1: - event.key = kbHome; - break; - case 2: - event.key = kbIns; - break; - case 3: - event.key = kbDel; - break; - case 4: - event.key = kbEnd; - break; - case 5: - event.key = kbPgUp; - break; - case 6: - event.key = kbPgDn; - break; - case 15: - event.key = kbF5; - break; - case 17: - event.key = kbF6; - break; - case 18: - event.key = kbF7; - break; - case 19: - event.key = kbF8; - break; - case 20: - event.key = kbF9; - break; - case 21: - event.key = kbF10; - break; - case 23: - event.key = kbF11; - break; - case 24: - event.key = kbF12; - break; - default: - // Unknown - return null; - } - - break; - case 2: - // Shift - switch (key) { - case 1: - event.key = kbShiftHome; - break; - case 2: - event.key = kbShiftIns; - break; - case 3: - event.key = kbShiftDel; - break; - case 4: - event.key = kbShiftEnd; - break; - case 5: - event.key = kbShiftPgUp; - break; - case 6: - event.key = kbShiftPgDn; - break; - case 15: - event.key = kbShiftF5; - break; - case 17: - event.key = kbShiftF6; - break; - case 18: - event.key = kbShiftF7; - break; - case 19: - event.key = kbShiftF8; - break; - case 20: - event.key = kbShiftF9; - break; - case 21: - event.key = kbShiftF10; - break; - case 23: - event.key = kbShiftF11; - break; - case 24: - event.key = kbShiftF12; - break; - default: - // Unknown - return null; - } - break; - - case 3: - // Alt - switch (key) { - case 1: - event.key = kbAltHome; - break; - case 2: - event.key = kbAltIns; - break; - case 3: - event.key = kbAltDel; - break; - case 4: - event.key = kbAltEnd; - break; - case 5: - event.key = kbAltPgUp; - break; - case 6: - event.key = kbAltPgDn; - break; - case 15: - event.key = kbAltF5; - break; - case 17: - event.key = kbAltF6; - break; - case 18: - event.key = kbAltF7; - break; - case 19: - event.key = kbAltF8; - break; - case 20: - event.key = kbAltF9; - break; - case 21: - event.key = kbAltF10; - break; - case 23: - event.key = kbAltF11; - break; - case 24: - event.key = kbAltF12; - break; - default: - // Unknown - return null; - } - break; - - case 5: - // Ctrl - switch (key) { - case 1: - event.key = kbCtrlHome; - break; - case 2: - event.key = kbCtrlIns; - break; - case 3: - event.key = kbCtrlDel; - break; - case 4: - event.key = kbCtrlEnd; - break; - case 5: - event.key = kbCtrlPgUp; - break; - case 6: - event.key = kbCtrlPgDn; - break; - case 15: - event.key = kbCtrlF5; - break; - case 17: - event.key = kbCtrlF6; - break; - case 18: - event.key = kbCtrlF7; - break; - case 19: - event.key = kbCtrlF8; - break; - case 20: - event.key = kbCtrlF9; - break; - case 21: - event.key = kbCtrlF10; - break; - case 23: - event.key = kbCtrlF11; - break; - case 24: - event.key = kbCtrlF12; - break; - default: - // Unknown - return null; - } - break; - - default: - // Unknown - return null; - } - - // All OK, return a keypress - return event; + int key = 0; + int modifier = 0; + if (params.size() > 0) { + key = Integer.parseInt(params.get(0)); + } + if (params.size() > 1) { + modifier = Integer.parseInt(params.get(1)); + } + boolean alt = false; + boolean ctrl = false; + boolean shift = false; + + switch (modifier) { + case 0: + // No modifier + break; + case 2: + // Shift + shift = true; + break; + case 3: + // Alt + alt = true; + break; + case 5: + // Ctrl + ctrl = true; + break; + default: + // Unknown modifier, bail out + return null; + } + + switch (key) { + case 1: + return new TKeypressEvent(kbHome, alt, ctrl, shift); + case 2: + return new TKeypressEvent(kbIns, alt, ctrl, shift); + case 3: + return new TKeypressEvent(kbDel, alt, ctrl, shift); + case 4: + return new TKeypressEvent(kbEnd, alt, ctrl, shift); + case 5: + return new TKeypressEvent(kbPgUp, alt, ctrl, shift); + case 6: + return new TKeypressEvent(kbPgDn, alt, ctrl, shift); + case 15: + return new TKeypressEvent(kbF5, alt, ctrl, shift); + case 17: + return new TKeypressEvent(kbF6, alt, ctrl, shift); + case 18: + return new TKeypressEvent(kbF7, alt, ctrl, shift); + case 19: + return new TKeypressEvent(kbF8, alt, ctrl, shift); + case 20: + return new TKeypressEvent(kbF9, alt, ctrl, shift); + case 21: + return new TKeypressEvent(kbF10, alt, ctrl, shift); + case 23: + return new TKeypressEvent(kbF11, alt, ctrl, shift); + case 24: + return new TKeypressEvent(kbF12, alt, ctrl, shift); + default: + // Unknown + return null; + } } /** @@ -656,532 +519,635 @@ public class ECMA48Terminal implements Runnable { * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event */ private TInputEvent parseMouse() { - int buttons = params.get(0).charAt(0) - 32; - int x = params.get(0).charAt(1) - 32 - 1; - int y = params.get(0).charAt(2) - 32 - 1; - - // Clamp X and Y to the physical screen coordinates. - if (x >= windowResize.width) { - x = windowResize.width - 1; - } - if (y >= windowResize.height) { - y = windowResize.height - 1; - } - - TMouseEvent event = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN); - event.x = x; - event.y = y; - event.absoluteX = x; - event.absoluteY = y; - - // System.err.printf("buttons: %04x\r\n", buttons); - - switch (buttons) { - case 0: - event.mouse1 = true; - mouse1 = true; - break; - case 1: - event.mouse2 = true; - mouse2 = true; - break; - case 2: - event.mouse3 = true; - mouse3 = true; - break; - case 3: - // Release or Move - if (!mouse1 && !mouse2 && !mouse3) { - event.type = TMouseEvent.Type.MOUSE_MOTION; - } else { - event.type = TMouseEvent.Type.MOUSE_UP; - } - if (mouse1) { - mouse1 = false; - event.mouse1 = true; - } - if (mouse2) { - mouse2 = false; - event.mouse2 = true; - } - if (mouse3) { - mouse3 = false; - event.mouse3 = true; - } - break; - - case 32: - // Dragging with mouse1 down - event.mouse1 = true; - mouse1 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 33: - // Dragging with mouse2 down - event.mouse2 = true; - mouse2 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 34: - // Dragging with mouse3 down - event.mouse3 = true; - mouse3 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 96: - // Dragging with mouse2 down after wheelUp - event.mouse2 = true; - mouse2 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 97: - // Dragging with mouse2 down after wheelDown - event.mouse2 = true; - mouse2 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 64: - event.mouseWheelUp = true; - break; - - case 65: - event.mouseWheelDown = true; - break; - - default: - // Unknown, just make it motion - event.type = TMouseEvent.Type.MOUSE_MOTION; - break; - } - return event; + int buttons = params.get(0).charAt(0) - 32; + int x = params.get(0).charAt(1) - 32 - 1; + int y = params.get(0).charAt(2) - 32 - 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; + + // System.err.printf("buttons: %04x\r\n", buttons); + + switch (buttons) { + case 0: + eventMouse1 = true; + mouse1 = true; + break; + case 1: + eventMouse2 = true; + mouse2 = true; + break; + case 2: + eventMouse3 = true; + mouse3 = true; + break; + case 3: + // Release or Move + if (!mouse1 && !mouse2 && !mouse3) { + eventType = TMouseEvent.Type.MOUSE_MOTION; + } else { + eventType = TMouseEvent.Type.MOUSE_UP; + } + if (mouse1) { + mouse1 = false; + eventMouse1 = true; + } + if (mouse2) { + mouse2 = false; + eventMouse2 = true; + } + if (mouse3) { + mouse3 = false; + eventMouse3 = true; + } + break; + + case 32: + // Dragging with mouse1 down + eventMouse1 = true; + mouse1 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 33: + // Dragging with mouse2 down + eventMouse2 = true; + mouse2 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 34: + // Dragging with mouse3 down + eventMouse3 = true; + mouse3 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 96: + // Dragging with mouse2 down after wheelUp + eventMouse2 = true; + mouse2 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 97: + // Dragging with mouse2 down after wheelDown + eventMouse2 = true; + mouse2 = true; + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + + case 64: + eventMouseWheelUp = true; + break; + + case 65: + eventMouseWheelDown = true; + break; + + default: + // Unknown, just make it motion + eventType = TMouseEvent.Type.MOUSE_MOTION; + break; + } + return new TMouseEvent(eventType, x, y, x, y, + eventMouse1, eventMouse2, eventMouse3, + eventMouseWheelUp, eventMouseWheelDown); } /** - * Return any events in the IO queue. + * Produce mouse events based on "Any event tracking" and SGR + * coordinates. See + * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking * - * @return list of new events (which may be empty) + * @param release if true, this was a release ('m') + * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event */ - 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)); + 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 events; + /** + * Return any events in the IO queue. + * + * @param queue list to append new events to + */ + public void getEvents(final List queue) { + synchronized (eventQueue) { + if (eventQueue.size() > 0) { + synchronized (queue) { + 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); + private void getIdleEvents(final List queue) { + Date now = new Date(); + + // Check for new window size + 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(); + } + + // 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(); + } + } } /** * 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(); - - TKeypressEvent keypress; - Date now = new Date(); - - /* - // ESCDELAY type timeout - if (state == ParseState.ESCAPE) { - long escDelay = now.getTime() - escapeTime; - if (escDelay > 250) { - // After 0.25 seconds, assume a true escape character - events.add(controlChar((char)0x1B)); - reset(); - } - } - */ - - 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) { - case GROUND: - - if (ch == 0x1B) { - state = ParseState.ESCAPE; - escapeTime = now.getTime(); - return events; - } - - if (ch <= 0x1F) { - // Control character - events.add(controlChar(ch)); - reset(); - return events; - } - - if (ch >= 0x20) { - // Normal character - keypress = new TKeypressEvent(); - keypress.key.isKey = false; - keypress.key.ch = ch; - events.add(keypress); - reset(); - return events; - } - - break; - - case ESCAPE: - if (ch <= 0x1F) { - // ALT-Control character - keypress = controlChar(ch); - keypress.key.alt = true; - events.add(keypress); - reset(); - return events; - } - - if (ch == 'O') { - // This will be one of the function keys - state = ParseState.ESCAPE_INTERMEDIATE; - return events; - } - - // '[' goes to CSI_ENTRY - if (ch == '[') { - state = ParseState.CSI_ENTRY; - return events; - } - - // Everything else is assumed to be Alt-keystroke - keypress = new TKeypressEvent(); - keypress.key.isKey = false; - keypress.key.ch = ch; - keypress.key.alt = true; - if ((ch >= 'A') && (ch <= 'Z')) { - keypress.key.shift = true; - } - events.add(keypress); - reset(); - return events; - - case ESCAPE_INTERMEDIATE: - if ((ch >= 'P') && (ch <= 'S')) { - // Function key - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - switch (ch) { - case 'P': - keypress.key.fnKey = TKeypress.F1; - break; - case 'Q': - keypress.key.fnKey = TKeypress.F2; - break; - case 'R': - keypress.key.fnKey = TKeypress.F3; - break; - case 'S': - keypress.key.fnKey = TKeypress.F4; - break; - default: - break; - } - events.add(keypress); - reset(); - return events; - } - - // Unknown keystroke, ignore - reset(); - return events; - - case CSI_ENTRY: - // Numbers - parameter values - if ((ch >= '0') && (ch <= '9')) { - params.set(paramI, params.get(paramI) + ch); - state = ParseState.CSI_PARAM; - return events; - } - // Parameter separator - if (ch == ';') { - paramI++; - params.set(paramI, ""); - return events; - } - - if ((ch >= 0x30) && (ch <= 0x7E)) { - switch (ch) { - case 'A': - // Up - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.UP; - if (params.size() > 1) { - if (params.get(1).equals("2")) { - keypress.key.shift = true; - } - if (params.get(1).equals("5")) { - keypress.key.ctrl = true; - } - if (params.get(1).equals("3")) { - keypress.key.alt = true; - } - } - events.add(keypress); - reset(); - return events; - case 'B': - // Down - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.DOWN; - if (params.size() > 1) { - if (params.get(1).equals("2")) { - keypress.key.shift = true; - } - if (params.get(1).equals("5")) { - keypress.key.ctrl = true; - } - if (params.get(1).equals("3")) { - keypress.key.alt = true; - } - } - events.add(keypress); - reset(); - return events; - case 'C': - // Right - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.RIGHT; - if (params.size() > 1) { - if (params.get(1).equals("2")) { - keypress.key.shift = true; - } - if (params.get(1).equals("5")) { - keypress.key.ctrl = true; - } - if (params.get(1).equals("3")) { - keypress.key.alt = true; - } - } - events.add(keypress); - reset(); - return events; - case 'D': - // Left - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.LEFT; - if (params.size() > 1) { - if (params.get(1).equals("2")) { - keypress.key.shift = true; - } - if (params.get(1).equals("5")) { - keypress.key.ctrl = true; - } - if (params.get(1).equals("3")) { - keypress.key.alt = true; - } - } - events.add(keypress); - reset(); - return events; - case 'H': - // Home - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.HOME; - events.add(keypress); - reset(); - return events; - case 'F': - // End - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.END; - events.add(keypress); - reset(); - return events; - case 'Z': - // CBT - Cursor backward X tab stops (default 1) - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.BTAB; - events.add(keypress); - reset(); - return events; - case 'M': - // Mouse position - state = ParseState.MOUSE; - return events; - default: - break; - } - } - - // Unknown keystroke, ignore - reset(); - return events; - - case CSI_PARAM: - // Numbers - parameter values - if ((ch >= '0') && (ch <= '9')) { - params.set(paramI, params.get(paramI) + ch); - state = ParseState.CSI_PARAM; - return events; - } - // Parameter separator - if (ch == ';') { - paramI++; - params.set(paramI, ""); - return events; - } - - if (ch == '~') { - events.add(csiFnKey()); - reset(); - return events; - } - - if ((ch >= 0x30) && (ch <= 0x7E)) { - switch (ch) { - case 'A': - // Up - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.UP; - if (params.size() > 1) { - if (params.get(1).equals("2")) { - keypress.key.shift = true; - } - if (params.get(1).equals("5")) { - keypress.key.ctrl = true; - } - if (params.get(1).equals("3")) { - keypress.key.alt = true; - } - } - events.add(keypress); - reset(); - return events; - case 'B': - // Down - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.DOWN; - if (params.size() > 1) { - if (params.get(1).equals("2")) { - keypress.key.shift = true; - } - if (params.get(1).equals("5")) { - keypress.key.ctrl = true; - } - if (params.get(1).equals("3")) { - keypress.key.alt = true; - } - } - events.add(keypress); - reset(); - return events; - case 'C': - // Right - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.RIGHT; - if (params.size() > 1) { - if (params.get(1).equals("2")) { - keypress.key.shift = true; - } - if (params.get(1).equals("5")) { - keypress.key.ctrl = true; - } - if (params.get(1).equals("3")) { - keypress.key.alt = true; - } - } - events.add(keypress); - reset(); - return events; - case 'D': - // Left - keypress = new TKeypressEvent(); - keypress.key.isKey = true; - keypress.key.fnKey = TKeypress.LEFT; - if (params.size() > 1) { - if (params.get(1).equals("2")) { - keypress.key.shift = true; - } - if (params.get(1).equals("5")) { - keypress.key.ctrl = true; - } - if (params.get(1).equals("3")) { - keypress.key.alt = true; - } - } - events.add(keypress); - reset(); - return events; - default: - break; - } - } - - // Unknown keystroke, ignore - reset(); - return events; - - case MOUSE: - params.set(0, params.get(paramI) + ch); - if (params.get(0).length() == 3) { - // We have enough to generate a mouse event - events.add(parseMouse()); - reset(); - } - return events; - - default: - break; - } - - // This "should" be impossible to reach - return events; + private void processChar(final List events, final char ch) { + + // ESCDELAY type timeout + Date now = new Date(); + if (state == ParseState.ESCAPE) { + long escDelay = now.getTime() - escapeTime; + if (escDelay > 250) { + // After 0.25 seconds, assume a true escape character + events.add(controlChar((char)0x1B, false)); + reset(); + } + } + + // TKeypress fields + boolean ctrl = false; + boolean alt = false; + boolean shift = false; + + // System.err.printf("state: %s ch %c\r\n", state, ch); + + switch (state) { + case GROUND: + + if (ch == 0x1B) { + state = ParseState.ESCAPE; + escapeTime = now.getTime(); + return; + } + + if (ch <= 0x1F) { + // Control character + events.add(controlChar(ch, false)); + reset(); + return; + } + + if (ch >= 0x20) { + // Normal character + events.add(new TKeypressEvent(false, 0, ch, + false, false, false)); + reset(); + return; + } + + break; + + case ESCAPE: + if (ch <= 0x1F) { + // ALT-Control character + events.add(controlChar(ch, true)); + reset(); + return; + } + + if (ch == 'O') { + // This will be one of the function keys + state = ParseState.ESCAPE_INTERMEDIATE; + return; + } + + // '[' goes to CSI_ENTRY + if (ch == '[') { + state = ParseState.CSI_ENTRY; + return; + } + + // Everything else is assumed to be Alt-keystroke + if ((ch >= 'A') && (ch <= 'Z')) { + shift = true; + } + alt = true; + events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift)); + reset(); + return; + + case ESCAPE_INTERMEDIATE: + if ((ch >= 'P') && (ch <= 'S')) { + // Function key + switch (ch) { + case 'P': + events.add(new TKeypressEvent(kbF1)); + break; + case 'Q': + events.add(new TKeypressEvent(kbF2)); + break; + case 'R': + events.add(new TKeypressEvent(kbF3)); + break; + case 'S': + events.add(new TKeypressEvent(kbF4)); + break; + default: + break; + } + reset(); + return; + } + + // Unknown keystroke, ignore + reset(); + return; + + case CSI_ENTRY: + // Numbers - parameter values + if ((ch >= '0') && (ch <= '9')) { + params.set(params.size() - 1, + params.get(params.size() - 1) + ch); + state = ParseState.CSI_PARAM; + return; + } + // Parameter separator + if (ch == ';') { + params.add(""); + return; + } + + if ((ch >= 0x30) && (ch <= 0x7E)) { + switch (ch) { + case 'A': + // Up + if (params.size() > 1) { + if (params.get(1).equals("2")) { + shift = true; + } + if (params.get(1).equals("5")) { + ctrl = true; + } + if (params.get(1).equals("3")) { + alt = true; + } + } + events.add(new TKeypressEvent(kbUp, alt, ctrl, shift)); + reset(); + return; + case 'B': + // Down + if (params.size() > 1) { + if (params.get(1).equals("2")) { + shift = true; + } + if (params.get(1).equals("5")) { + ctrl = true; + } + if (params.get(1).equals("3")) { + alt = true; + } + } + events.add(new TKeypressEvent(kbDown, alt, ctrl, shift)); + reset(); + return; + case 'C': + // Right + if (params.size() > 1) { + if (params.get(1).equals("2")) { + shift = true; + } + if (params.get(1).equals("5")) { + ctrl = true; + } + if (params.get(1).equals("3")) { + alt = true; + } + } + events.add(new TKeypressEvent(kbRight, alt, ctrl, shift)); + reset(); + return; + case 'D': + // Left + if (params.size() > 1) { + if (params.get(1).equals("2")) { + shift = true; + } + if (params.get(1).equals("5")) { + ctrl = true; + } + if (params.get(1).equals("3")) { + alt = true; + } + } + events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift)); + reset(); + return; + case 'H': + // Home + events.add(new TKeypressEvent(kbHome)); + reset(); + return; + case 'F': + // End + events.add(new TKeypressEvent(kbEnd)); + reset(); + return; + case 'Z': + // CBT - Cursor backward X tab stops (default 1) + events.add(new TKeypressEvent(kbBackTab)); + reset(); + return; + case 'M': + // Mouse position + state = ParseState.MOUSE; + return; + case '<': + // Mouse position, SGR (1006) coordinates + state = ParseState.MOUSE_SGR; + return; + default: + break; + } + } + + // Unknown keystroke, ignore + 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(params.size() - 1, + params.get(params.size() - 1) + ch); + state = ParseState.CSI_PARAM; + return; + } + // Parameter separator + if (ch == ';') { + params.add(""); + return; + } + + if (ch == '~') { + events.add(csiFnKey()); + reset(); + return; + } + + if ((ch >= 0x30) && (ch <= 0x7E)) { + switch (ch) { + case 'A': + // Up + if (params.size() > 1) { + if (params.get(1).equals("2")) { + shift = true; + } + if (params.get(1).equals("5")) { + ctrl = true; + } + if (params.get(1).equals("3")) { + alt = true; + } + } + events.add(new TKeypressEvent(kbUp, alt, ctrl, shift)); + reset(); + return; + case 'B': + // Down + if (params.size() > 1) { + if (params.get(1).equals("2")) { + shift = true; + } + if (params.get(1).equals("5")) { + ctrl = true; + } + if (params.get(1).equals("3")) { + alt = true; + } + } + events.add(new TKeypressEvent(kbDown, alt, ctrl, shift)); + reset(); + return; + case 'C': + // Right + if (params.size() > 1) { + if (params.get(1).equals("2")) { + shift = true; + } + if (params.get(1).equals("5")) { + ctrl = true; + } + if (params.get(1).equals("3")) { + alt = true; + } + } + events.add(new TKeypressEvent(kbRight, alt, ctrl, shift)); + reset(); + return; + case 'D': + // Left + if (params.size() > 1) { + if (params.get(1).equals("2")) { + shift = true; + } + if (params.get(1).equals("5")) { + ctrl = true; + } + if (params.get(1).equals("3")) { + alt = true; + } + } + events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift)); + reset(); + return; + default: + break; + } + } + + // Unknown keystroke, ignore + reset(); + return; + + case MOUSE: + 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()); + reset(); + } + return; + + default: + break; + } + + // This "should" be impossible to reach + return; } /** @@ -1192,11 +1158,11 @@ public class ECMA48Terminal implements Runnable { * @param on if true, enable metaSendsEscape * @return the string to emit to xterm */ - static public String xtermMetaSendsEscape(boolean on) { - if (on) { - return "\033[?1036h\033[?1034l"; - } - return "\033[?1036l"; + private String xtermMetaSendsEscape(final boolean on) { + if (on) { + return "\033[?1036h\033[?1034l"; + } + return "\033[?1036l"; } /** @@ -1208,26 +1174,27 @@ public class ECMA48Terminal implements Runnable { * @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) { - // Nix any trailing ';' because that resets all attributes - while (str.endsWith(":")) { - str = str.substring(0, str.length() - 1); - } - } - return "\033[" + str + "m"; + private String addHeaderSGR(String str) { + if (str.length() > 0) { + // Nix any trailing ';' because that resets all attributes + while (str.endsWith(":")) { + str = str.substring(0, str.length() - 1); + } + } + return "\033[" + str + "m"; } /** - * 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" */ - static public String color(Color color, boolean foreground) { - return color(color, foreground, true); + String color(final Color color, final boolean foreground) { + return color(color, foreground, true); } /** @@ -1240,36 +1207,36 @@ public class ECMA48Terminal implements Runnable { * @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) { - - int ecmaColor = color.value; - - // Convert Color.* values to SGR numerics - if (foreground == true) { - ecmaColor += 30; - } else { - ecmaColor += 40; - } - - if (header) { - return String.format("\033[%dm", ecmaColor); - } else { - return String.format("%d;", ecmaColor); - } + private String color(final Color color, final boolean foreground, + final boolean header) { + + int ecmaColor = color.getValue(); + + // Convert Color.* values to SGR numerics + if (foreground) { + ecmaColor += 30; + } else { + ecmaColor += 40; + } + + if (header) { + return String.format("\033[%dm", ecmaColor); + } else { + return String.format("%d;", ecmaColor); + } } /** - * 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" */ - static public String color(Color foreColor, Color backColor) { - return color(foreColor, backColor, true); + String color(final Color foreColor, final Color backColor) { + return color(foreColor, backColor, true); } /** @@ -1283,27 +1250,28 @@ public class ECMA48Terminal implements Runnable { * @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) { + private String color(final Color foreColor, final Color backColor, + final boolean header) { - int ecmaForeColor = foreColor.value; - int ecmaBackColor = backColor.value; + int ecmaForeColor = foreColor.getValue(); + int ecmaBackColor = backColor.getValue(); - // Convert Color.* values to SGR numerics - ecmaBackColor += 40; - ecmaForeColor += 30; + // Convert Color.* values to SGR numerics + ecmaBackColor += 40; + ecmaForeColor += 30; - if (header) { - return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor); - } else { - return String.format("%d;%d;", ecmaForeColor, ecmaBackColor); - } + if (header) { + return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor); + } else { + return String.format("%d;%d;", ecmaForeColor, ecmaBackColor); + } } /** * 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 @@ -1314,53 +1282,54 @@ public class ECMA48Terminal implements Runnable { * @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) { - - int ecmaForeColor = foreColor.value; - int ecmaBackColor = backColor.value; - - // Convert Color.* values to SGR numerics - ecmaBackColor += 40; - ecmaForeColor += 30; - - StringBuilder sb = new StringBuilder(); - if ( bold && reverse && blink && !underline ) { - sb.append("\033[0;1;7;5;"); - } else if ( bold && reverse && !blink && !underline ) { - sb.append("\033[0;1;7;"); - } else if ( !bold && reverse && blink && !underline ) { - sb.append("\033[0;7;5;"); - } else if ( bold && !reverse && blink && !underline ) { - sb.append("\033[0;1;5;"); - } else if ( bold && !reverse && !blink && !underline ) { - sb.append("\033[0;1;"); - } else if ( !bold && reverse && !blink && !underline ) { - sb.append("\033[0;7;"); - } else if ( !bold && !reverse && blink && !underline) { - sb.append("\033[0;5;"); - } else if ( bold && reverse && blink && underline ) { - sb.append("\033[0;1;7;5;4;"); - } else if ( bold && reverse && !blink && underline ) { - sb.append("\033[0;1;7;4;"); - } else if ( !bold && reverse && blink && underline ) { - sb.append("\033[0;7;5;4;"); - } else if ( bold && !reverse && blink && underline ) { - sb.append("\033[0;1;5;4;"); - } else if ( bold && !reverse && !blink && underline ) { - sb.append("\033[0;1;4;"); - } else if ( !bold && reverse && !blink && underline ) { - sb.append("\033[0;7;4;"); - } else if ( !bold && !reverse && blink && underline) { - sb.append("\033[0;5;4;"); - } else if ( !bold && !reverse && !blink && underline) { - sb.append("\033[0;4;"); - } else { - assert(!bold && !reverse && !blink && !underline); - sb.append("\033[0;"); - } - sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor)); - return sb.toString(); + String color(final Color foreColor, final Color backColor, + final boolean bold, final boolean reverse, final boolean blink, + final boolean underline) { + + int ecmaForeColor = foreColor.getValue(); + int ecmaBackColor = backColor.getValue(); + + // Convert Color.* values to SGR numerics + ecmaBackColor += 40; + ecmaForeColor += 30; + + StringBuilder sb = new StringBuilder(); + if ( bold && reverse && blink && !underline ) { + sb.append("\033[0;1;7;5;"); + } else if ( bold && reverse && !blink && !underline ) { + sb.append("\033[0;1;7;"); + } else if ( !bold && reverse && blink && !underline ) { + sb.append("\033[0;7;5;"); + } else if ( bold && !reverse && blink && !underline ) { + sb.append("\033[0;1;5;"); + } else if ( bold && !reverse && !blink && !underline ) { + sb.append("\033[0;1;"); + } else if ( !bold && reverse && !blink && !underline ) { + sb.append("\033[0;7;"); + } else if ( !bold && !reverse && blink && !underline) { + sb.append("\033[0;5;"); + } else if ( bold && reverse && blink && underline ) { + sb.append("\033[0;1;7;5;4;"); + } else if ( bold && reverse && !blink && underline ) { + sb.append("\033[0;1;7;4;"); + } else if ( !bold && reverse && blink && underline ) { + sb.append("\033[0;7;5;4;"); + } else if ( bold && !reverse && blink && underline ) { + sb.append("\033[0;1;5;4;"); + } else if ( bold && !reverse && !blink && underline ) { + sb.append("\033[0;1;4;"); + } else if ( !bold && reverse && !blink && underline ) { + sb.append("\033[0;7;4;"); + } else if ( !bold && !reverse && blink && underline) { + sb.append("\033[0;5;4;"); + } else if ( !bold && !reverse && !blink && underline) { + sb.append("\033[0;4;"); + } else { + assert (!bold && !reverse && !blink && !underline); + sb.append("\033[0;"); + } + sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor)); + return sb.toString(); } /** @@ -1370,21 +1339,22 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[7m" */ - static public String reverse(boolean on) { - if (on) { - return "\033[7m"; - } - return "\033[27m"; + private String reverse(final boolean on) { + if (on) { + return "\033[7m"; + } + return "\033[27m"; } /** - * 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" */ - static public String normal() { - return normal(true); + String normal() { + return normal(true); } /** @@ -1395,11 +1365,11 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[0m" */ - static public String normal(boolean header) { - if (header) { - return "\033[0;37;40m"; - } - return "0;37;40"; + private String normal(final boolean header) { + if (header) { + return "\033[0;37;40m"; + } + return "0;37;40"; } /** @@ -1409,8 +1379,8 @@ public class ECMA48Terminal implements Runnable { * @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); + private String bold(final boolean on) { + return bold(on, true); } /** @@ -1422,17 +1392,17 @@ public class ECMA48Terminal implements Runnable { * @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) { - if (on) { - return "\033[1m"; - } - return "\033[22m"; - } - if (on) { - return "1;"; - } - return "22;"; + private String bold(final boolean on, final boolean header) { + if (header) { + if (on) { + return "\033[1m"; + } + return "\033[22m"; + } + if (on) { + return "1;"; + } + return "22;"; } /** @@ -1442,8 +1412,8 @@ public class ECMA48Terminal implements Runnable { * @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); + private String blink(final boolean on) { + return blink(on, true); } /** @@ -1455,17 +1425,17 @@ public class ECMA48Terminal implements Runnable { * @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) { - if (on) { - return "\033[5m"; - } - return "\033[25m"; - } - if (on) { - return "5;"; - } - return "25;"; + private String blink(final boolean on, final boolean header) { + if (header) { + if (on) { + return "\033[5m"; + } + return "\033[25m"; + } + if (on) { + return "5;"; + } + return "25;"; } /** @@ -1476,29 +1446,30 @@ public class ECMA48Terminal implements Runnable { * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[4m" */ - static public String underline(boolean on) { - if (on) { - return "\033[4m"; - } - return "\033[24m"; + private String underline(final boolean on) { + if (on) { + return "\033[4m"; + } + return "\033[24m"; } /** - * 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(boolean on) { - if (on && (cursorOn == false)) { - cursorOn = true; - return "\033[?25h"; - } - if (!on && (cursorOn == true)) { - cursorOn = false; - return "\033[?25l"; - } - return ""; + String cursor(final boolean on) { + if (on && !cursorOn) { + cursorOn = true; + return "\033[?25h"; + } + if (!on && cursorOn) { + cursorOn = false; + return "\033[?25l"; + } + return ""; } /** @@ -1507,19 +1478,19 @@ public class ECMA48Terminal implements Runnable { * * @return the string to emit to an ANSI / ECMA-style terminal */ - static public String clearAll() { - return "\033[0;37;40m\033[2J"; + public String clearAll() { + return "\033[0;37;40m\033[2J"; } /** * 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 */ - static public String clearRemainingLine() { - return "\033[0;37;40m\033[K"; + String clearRemainingLine() { + return "\033[0;37;40m\033[K"; } /** @@ -1528,8 +1499,8 @@ public class ECMA48Terminal implements Runnable { * * @return the string to emit to an ANSI / ECMA-style terminal */ - static public String clearPreceedingLine() { - return "\033[0;37;40m\033[1K"; + private String clearPreceedingLine() { + return "\033[0;37;40m\033[1K"; } /** @@ -1538,8 +1509,8 @@ public class ECMA48Terminal implements Runnable { * * @return the string to emit to an ANSI / ECMA-style terminal */ - static public String clearLine() { - return "\033[0;37;40m\033[2K"; + private String clearLine() { + return "\033[0;37;40m\033[2K"; } /** @@ -1547,24 +1518,26 @@ public class ECMA48Terminal implements Runnable { * * @return the string to emit to an ANSI / ECMA-style terminal */ - static public String home() { - return "\033[H"; + 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 */ - static public String gotoXY(int x, int y) { - return String.format("\033[%d;%dH", y + 1, x + 1); + 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. @@ -1574,66 +1547,81 @@ public class ECMA48Terminal implements Runnable { * buffer. * @return the string to emit to xterm */ - static public String mouse(boolean on) { - if (on) { - return "\033[?1003;1005h\033[?1049h"; - } - return "\033[?1003;1005l\033[?1049l"; + private String mouse(final boolean on) { + if (on) { + return "\033[?1003;1005;1006h\033[?1049h"; + } + return "\033[?1003;1006;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(); + 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 && !stopReaderThread) { + 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, readBuffer.length); + // 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); + } + 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); + } + // 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(); } }