X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fio%2FECMA48Terminal.java;h=174da85b266d03fe00889cbdb4bef0ecaa7cd7f6;hb=a2018e9964f6c58742cd1e6dd0a0c63e244a89d6;hp=b8fabda389b5253d10bf5ed0a1b91199eee37bff;hpb=b299e69c251b7639440bfb914dbbdc51aa689af5;p=nikiroo-utils.git diff --git a/src/jexer/io/ECMA48Terminal.java b/src/jexer/io/ECMA48Terminal.java index b8fabda..174da85 100644 --- a/src/jexer/io/ECMA48Terminal.java +++ b/src/jexer/io/ECMA48Terminal.java @@ -1,29 +1,27 @@ -/** +/* * Jexer - Java Text User Interface * - * License: LGPLv3 or later - * - * 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. + * The MIT License (MIT) * - * Copyright (C) 2015 Kevin Lamonte + * Copyright (C) 2017 Kevin Lamonte * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public License - * as published by the Free Software Foundation; either version 3 of - * the License, or (at your option) any later version. + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * General Public License for more details. + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. * - * You should have received a copy of the GNU Lesser General Public - * License along with this program; if not, see - * http://www.gnu.org/licenses/, or write to the Free Software - * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. * * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 @@ -46,7 +44,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 +58,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 +70,7 @@ public class ECMA48Terminal implements Runnable { * * @return the SessionInfo */ - public final SessionInfo getSessionInfo() { + public SessionInfo getSessionInfo() { return sessionInfo; } @@ -98,11 +95,6 @@ public class ECMA48Terminal implements Runnable { */ private ArrayList params; - /** - * params[paramI] is being appended to. - */ - private int paramI; - /** * States in the input parser. */ @@ -112,8 +104,8 @@ public class ECMA48Terminal implements Runnable { ESCAPE_INTERMEDIATE, CSI_ENTRY, CSI_PARAM, - // CSI_INTERMEDIATE, - MOUSE + MOUSE, + MOUSE_SGR, } /** @@ -127,6 +119,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 +180,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 +238,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 +273,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 +285,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 +293,7 @@ public class ECMA48Terminal implements Runnable { mouse2 = false; mouse3 = false; stopReaderThread = false; + this.listener = listener; if (input == null) { // inputStream = System.in; @@ -307,9 +305,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 +329,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, @@ -342,6 +341,95 @@ public class ECMA48Terminal implements Runnable { readerThread.start(); } + /** + * Constructor sets up state for getEvent(). + * + * @param listener the object this backend needs to wake up when new + * input comes in + * @param input the InputStream underlying 'reader'. Its available() + * method is used to determine if reader.read() will block or not. + * @param reader a Reader connected to the remote user. + * @param writer a PrintWriter connected to the remote user. + * @param setRawMode if true, set System.in into raw mode with stty. + * This should in general not be used. It is here solely for Demo3, + * which uses System.in. + * @throws IllegalArgumentException if input, reader, or writer are null. + */ + public ECMA48Terminal(final Object listener, final InputStream input, + final Reader reader, final PrintWriter writer, + final boolean setRawMode) { + + if (input == null) { + throw new IllegalArgumentException("InputStream must be specified"); + } + if (reader == null) { + throw new IllegalArgumentException("Reader must be specified"); + } + if (writer == null) { + throw new IllegalArgumentException("Writer must be specified"); + } + reset(); + mouse1 = false; + mouse2 = false; + mouse3 = false; + stopReaderThread = false; + this.listener = listener; + + inputStream = input; + this.input = reader; + + if (setRawMode == true) { + sttyRaw(); + } + this.setRawMode = setRawMode; + + 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 (setRawMode == true) { + // Reading right off the tty + sessionInfo = new TTYSessionInfo(); + } else { + sessionInfo = new TSessionInfo(); + } + } + + this.output = writer; + + // 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(); + } + + /** + * Constructor sets up state for getEvent(). + * + * @param listener the object this backend needs to wake up when new + * input comes in + * @param input the InputStream underlying 'reader'. Its available() + * method is used to determine if reader.read() will block or not. + * @param reader a Reader connected to the remote user. + * @param writer a PrintWriter connected to the remote user. + * @throws IllegalArgumentException if input, reader, or writer are null. + */ + public ECMA48Terminal(final Object listener, final InputStream input, + final Reader reader, final PrintWriter writer) { + + this(listener, input, reader, writer, false); + } + /** * Restore terminal to normal state. */ @@ -396,7 +484,6 @@ public class ECMA48Terminal implements Runnable { private void reset() { state = ParseState.GROUND; params = new ArrayList(); - paramI = 0; params.clear(); params.add(""); } @@ -443,38 +530,18 @@ public class ECMA48Terminal implements Runnable { */ 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)); - } 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; + if (params.size() > 1) { + shift = csiIsShift(params.get(1)); + alt = csiIsAlt(params.get(1)); + ctrl = csiIsCtrl(params.get(1)); } - + switch (key) { case 1: return new TKeypressEvent(kbHome, alt, ctrl, shift); @@ -530,97 +597,199 @@ public class ECMA48Terminal implements Runnable { y = windowResize.getHeight() - 1; } - TMouseEvent event = new TMouseEvent(TMouseEvent.Type.MOUSE_DOWN); - event.x = x; - event.y = y; - event.absoluteX = x; - event.absoluteY = y; + 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: - event.mouse1 = true; + eventMouse1 = true; mouse1 = true; break; case 1: - event.mouse2 = true; + eventMouse2 = true; mouse2 = true; break; case 2: - event.mouse3 = true; + eventMouse3 = true; mouse3 = true; break; case 3: // Release or Move if (!mouse1 && !mouse2 && !mouse3) { - event.type = TMouseEvent.Type.MOUSE_MOTION; + eventType = TMouseEvent.Type.MOUSE_MOTION; } else { - event.type = TMouseEvent.Type.MOUSE_UP; + eventType = TMouseEvent.Type.MOUSE_UP; } if (mouse1) { mouse1 = false; - event.mouse1 = true; + eventMouse1 = true; } if (mouse2) { mouse2 = false; - event.mouse2 = true; + eventMouse2 = true; } if (mouse3) { mouse3 = false; - event.mouse3 = true; + eventMouse3 = true; } break; case 32: // Dragging with mouse1 down - event.mouse1 = true; + eventMouse1 = true; mouse1 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; + eventType = TMouseEvent.Type.MOUSE_MOTION; break; case 33: // Dragging with mouse2 down - event.mouse2 = true; + eventMouse2 = true; mouse2 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; + eventType = TMouseEvent.Type.MOUSE_MOTION; break; case 34: // Dragging with mouse3 down - event.mouse3 = true; + eventMouse3 = true; mouse3 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; + eventType = TMouseEvent.Type.MOUSE_MOTION; break; case 96: // Dragging with mouse2 down after wheelUp - event.mouse2 = true; + eventMouse2 = true; mouse2 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; + eventType = TMouseEvent.Type.MOUSE_MOTION; break; case 97: // Dragging with mouse2 down after wheelDown - event.mouse2 = true; + eventMouse2 = true; mouse2 = true; - event.type = TMouseEvent.Type.MOUSE_MOTION; + eventType = TMouseEvent.Type.MOUSE_MOTION; break; case 64: - event.mouseWheelUp = true; + eventMouseWheelUp = true; break; case 65: - event.mouseWheelDown = true; + eventMouseWheelDown = true; break; default: // Unknown, just make it motion - event.type = TMouseEvent.Type.MOUSE_MOTION; + eventType = TMouseEvent.Type.MOUSE_MOTION; break; } - return event; + return new TMouseEvent(eventType, x, y, x, y, + eventMouse1, eventMouse2, eventMouse3, + 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); } /** @@ -631,7 +800,9 @@ public class ECMA48Terminal implements Runnable { public void getEvents(final List queue) { synchronized (eventQueue) { if (eventQueue.size() > 0) { - queue.addAll(eventQueue); + synchronized (queue) { + queue.addAll(eventQueue); + } eventQueue.clear(); } } @@ -642,32 +813,83 @@ 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) { - 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(); } } } + /** + * Returns true if the CSI parameter for a keyboard command means that + * shift was down. + */ + private boolean csiIsShift(final String x) { + if ((x.equals("2")) + || (x.equals("4")) + || (x.equals("6")) + || (x.equals("8")) + ) { + return true; + } + return false; + } + + /** + * Returns true if the CSI parameter for a keyboard command means that + * alt was down. + */ + private boolean csiIsAlt(final String x) { + if ((x.equals("3")) + || (x.equals("4")) + || (x.equals("7")) + || (x.equals("8")) + ) { + return true; + } + return false; + } + + /** + * Returns true if the CSI parameter for a keyboard command means that + * ctrl was down. + */ + private boolean csiIsCtrl(final String x) { + if ((x.equals("5")) + || (x.equals("6")) + || (x.equals("7")) + || (x.equals("8")) + ) { + return true; + } + return false; + } + /** * Parses the next character of input to see if an InputEvent is * fully here. @@ -692,8 +914,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); @@ -782,14 +1002,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; } @@ -797,65 +1017,21 @@ public class ECMA48Terminal implements Runnable { 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; @@ -878,6 +1054,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; } @@ -887,17 +1067,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; } @@ -912,15 +1130,9 @@ public class ECMA48Terminal implements Runnable { 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; - } + shift = csiIsShift(params.get(1)); + alt = csiIsAlt(params.get(1)); + ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbUp, alt, ctrl, shift)); reset(); @@ -928,15 +1140,9 @@ public class ECMA48Terminal implements Runnable { 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; - } + shift = csiIsShift(params.get(1)); + alt = csiIsAlt(params.get(1)); + ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbDown, alt, ctrl, shift)); reset(); @@ -944,15 +1150,9 @@ public class ECMA48Terminal implements Runnable { 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; - } + shift = csiIsShift(params.get(1)); + alt = csiIsAlt(params.get(1)); + ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbRight, alt, ctrl, shift)); reset(); @@ -960,19 +1160,33 @@ public class ECMA48Terminal implements Runnable { 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; - } + shift = csiIsShift(params.get(1)); + alt = csiIsAlt(params.get(1)); + ctrl = csiIsCtrl(params.get(1)); } events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift)); reset(); return; + case 'H': + // Home + if (params.size() > 1) { + shift = csiIsShift(params.get(1)); + alt = csiIsAlt(params.get(1)); + ctrl = csiIsCtrl(params.get(1)); + } + events.add(new TKeypressEvent(kbHome, alt, ctrl, shift)); + reset(); + return; + case 'F': + // End + if (params.size() > 1) { + shift = csiIsShift(params.get(1)); + alt = csiIsAlt(params.get(1)); + ctrl = csiIsCtrl(params.get(1)); + } + events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift)); + reset(); + return; default: break; } @@ -983,7 +1197,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()); @@ -1007,7 +1221,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"; } @@ -1015,33 +1229,26 @@ public class ECMA48Terminal implements Runnable { } /** - * Convert a list of SGR parameters into a full escape sequence. This - * also eliminates a trailing ';' which would otherwise reset everything - * to white-on-black not-bold. + * Create an xterm OSC sequence to change the window title. Note package + * private access. * - * @param str string of parameters, e.g. "31;1;" - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[31;1m" + * @param title the new title + * @return the string to emit to xterm */ - 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"; + String setTitle(final String title) { + return "\033]2;" + title + "\007"; } /** - * 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); } @@ -1055,7 +1262,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(); @@ -1075,15 +1282,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); } @@ -1098,7 +1305,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(); @@ -1118,7 +1325,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 @@ -1129,7 +1337,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) { @@ -1180,26 +1388,13 @@ public class ECMA48Terminal implements Runnable { } /** - * Create a SGR parameter sequence for enabling reverse color. - * - * @param on if true, turn on reverse - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[7m" - */ - public 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" */ - public String normal() { + String normal() { return normal(true); } @@ -1211,7 +1406,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"; } @@ -1219,93 +1414,13 @@ public class ECMA48Terminal implements Runnable { } /** - * Create a SGR parameter sequence for enabling boldface. - * - * @param on if true, turn on bold - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[1m" - */ - public String bold(final boolean on) { - return bold(on, true); - } - - /** - * Create a SGR parameter sequence for enabling boldface. - * - * @param on if true, turn on bold - * @param header if true, make the full header, otherwise just emit the - * bare parameter e.g. "1;" - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[1m" - */ - public String bold(final boolean on, final boolean header) { - if (header) { - if (on) { - return "\033[1m"; - } - return "\033[22m"; - } - if (on) { - return "1;"; - } - return "22;"; - } - - /** - * Create a SGR parameter sequence for enabling blinking text. - * - * @param on if true, turn on blink - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[5m" - */ - public String blink(final boolean on) { - return blink(on, true); - } - - /** - * Create a SGR parameter sequence for enabling blinking text. - * - * @param on if true, turn on blink - * @param header if true, make the full header, otherwise just emit the - * bare parameter e.g. "5;" - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[5m" - */ - public String blink(final boolean on, final boolean header) { - if (header) { - if (on) { - return "\033[5m"; - } - return "\033[25m"; - } - if (on) { - return "5;"; - } - return "25;"; - } - - /** - * Create a SGR parameter sequence for enabling underline / underscored - * text. - * - * @param on if true, turn on underline - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[4m" - */ - public 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(final boolean on) { + String cursor(final boolean on) { if (on && !cursorOn) { cursorOn = true; return "\033[?25h"; @@ -1330,57 +1445,30 @@ 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"; } /** - * Clear the line up the cursor (inclusive). Because some terminals use - * back-color-erase, set the color to white-on-black beforehand. - * - * @return the string to emit to an ANSI / ECMA-style terminal - */ - public String clearPreceedingLine() { - return "\033[0;37;40m\033[1K"; - } - - /** - * Clear the line. Because some terminals use back-color-erase, set the - * color to white-on-black beforehand. - * - * @return the string to emit to an ANSI / ECMA-style terminal - */ - public String clearLine() { - return "\033[0;37;40m\033[2K"; - } - - /** - * Move the cursor to the top-left corner. - * - * @return the string to emit to an ANSI / ECMA-style terminal - */ - public 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. @@ -1390,11 +1478,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[?1002;1003;1005;1006h\033[?1049h"; } - return "\033[?1003;1005l\033[?1049l"; + return "\033[?1002;1003;1006;1005l\033[?1049l"; } /** @@ -1418,7 +1506,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 @@ -1427,23 +1515,34 @@ 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 { - // Wait 5 millis for more data - Thread.sleep(5); + 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) {