+ * @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(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);
+ }
+ }
+
+ /**
+ * Produce special key from CSI Pn ; Pm ; ... ~
+ *
+ * @return one KEYPRESS event representing a special key
+ */
+ private TInputEvent csiFnKey() {
+ int key = 0;
+ if (params.size() > 0) {
+ key = Integer.parseInt(params.get(0));
+ }
+ boolean alt = false;
+ boolean ctrl = false;
+ boolean shift = false;
+ 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);
+ 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;
+ }
+ }
+
+ /**
+ * Produce mouse events based on "Any event tracking" and UTF-8
+ * coordinates. See
+ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
+ *
+ * @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.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);
+ }
+
+ /**
+ * Produce mouse events based on "Any event tracking" and SGR
+ * coordinates. See
+ * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking
+ *
+ * @param release if true, this was a release ('m')
+ * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event
+ */
+ private TInputEvent parseMouseSGR(final boolean release) {
+ // SGR extended coordinates - mode 1006
+ if (params.size() < 3) {
+ // Invalid position, bail out.
+ return null;
+ }
+ int buttons = Integer.parseInt(params.get(0));
+ int x = Integer.parseInt(params.get(1)) - 1;
+ int y = Integer.parseInt(params.get(2)) - 1;
+
+ // Clamp X and Y to the physical screen coordinates.
+ if (x >= windowResize.getWidth()) {
+ x = windowResize.getWidth() - 1;
+ }
+ if (y >= windowResize.getHeight()) {
+ y = windowResize.getHeight() - 1;
+ }
+
+ TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN;
+ boolean eventMouse1 = false;
+ boolean eventMouse2 = false;
+ boolean eventMouse3 = false;
+ boolean eventMouseWheelUp = false;
+ boolean eventMouseWheelDown = false;
+
+ if (release) {
+ eventType = TMouseEvent.Type.MOUSE_UP;
+ }
+
+ switch (buttons) {
+ case 0:
+ eventMouse1 = true;
+ break;
+ case 1:
+ eventMouse2 = true;
+ break;
+ case 2:
+ eventMouse3 = true;
+ break;
+ case 35:
+ // Motion only, no buttons down
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 32:
+ // Dragging with mouse1 down
+ eventMouse1 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 33:
+ // Dragging with mouse2 down
+ eventMouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 34:
+ // Dragging with mouse3 down
+ eventMouse3 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 96:
+ // Dragging with mouse2 down after wheelUp
+ eventMouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 97:
+ // Dragging with mouse2 down after wheelDown
+ eventMouse2 = true;
+ eventType = TMouseEvent.Type.MOUSE_MOTION;
+ break;
+
+ case 64:
+ eventMouseWheelUp = true;
+ break;
+
+ case 65:
+ eventMouseWheelDown = true;
+ break;
+
+ default:
+ // Unknown, bail out
+ return null;
+ }
+ return new TMouseEvent(eventType, x, y, x, y,
+ eventMouse1, eventMouse2, eventMouse3,
+ eventMouseWheelUp, eventMouseWheelDown);
+ }
+
+ /**
+ * Return any events in the IO queue due to timeout.
+ *
+ * @param queue list to append new events to
+ */
+ private void getIdleEvents(final List<TInputEvent> queue) {
+ long nowTime = System.currentTimeMillis();
+
+ // Check for new window size
+ long windowSizeDelay = nowTime - windowSizeTime;
+ if (windowSizeDelay > 1000) {
+ int oldTextWidth = getTextWidth();
+ int oldTextHeight = getTextHeight();
+
+ sessionInfo.queryWindowSize();
+ int newWidth = sessionInfo.getWindowWidth();
+ int newHeight = sessionInfo.getWindowHeight();
+
+ if ((newWidth != windowResize.getWidth())
+ || (newHeight != windowResize.getHeight())
+ ) {
+
+ // Request xterm report window dimensions in pixels again.
+ // Between now and then, ensure that the reported text cell
+ // size is the same by setting widthPixels and heightPixels
+ // to match the new dimensions.
+ widthPixels = oldTextWidth * newWidth;
+ heightPixels = oldTextHeight * newHeight;
+
+ if (debugToStderr) {
+ System.err.println("Screen size changed, old size " +
+ windowResize);
+ System.err.println(" new size " +
+ newWidth + " x " + newHeight);
+ System.err.println(" old pixels " +
+ oldTextWidth + " x " + oldTextHeight);
+ System.err.println(" new pixels " +
+ getTextWidth() + " x " + getTextHeight());
+ }
+
+ this.output.printf("%s", xtermReportPixelDimensions());
+ this.output.flush();
+
+ TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
+ newWidth, newHeight);
+ windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
+ newWidth, newHeight);
+ queue.add(event);
+ }
+ windowSizeTime = nowTime;
+ }
+
+ // ESCDELAY type timeout
+ if (state == ParseState.ESCAPE) {
+ long escDelay = nowTime - escapeTime;
+ if (escDelay > 100) {
+ // After 0.1 seconds, assume a true escape character
+ queue.add(controlChar((char)0x1B, false));
+ resetParser();
+ }
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param events list to append new events to
+ * @param ch Unicode code point
+ */
+ private void processChar(final List<TInputEvent> events, final char ch) {
+
+ // ESCDELAY type timeout
+ long nowTime = System.currentTimeMillis();
+ if (state == ParseState.ESCAPE) {
+ long escDelay = nowTime - escapeTime;
+ if (escDelay > 250) {
+ // After 0.25 seconds, assume a true escape character
+ events.add(controlChar((char)0x1B, false));
+ resetParser();
+ }
+ }
+
+ // 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 = nowTime;
+ return;
+ }
+
+ if (ch <= 0x1F) {
+ // Control character
+ events.add(controlChar(ch, false));
+ resetParser();
+ return;
+ }
+
+ if (ch >= 0x20) {
+ // Normal character
+ events.add(new TKeypressEvent(false, 0, ch,
+ false, false, false));
+ resetParser();
+ return;
+ }
+
+ break;
+
+ case ESCAPE:
+ if (ch <= 0x1F) {
+ // ALT-Control character
+ events.add(controlChar(ch, true));
+ resetParser();
+ 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));
+ resetParser();
+ 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;
+ }
+ resetParser();
+ return;
+ }
+
+ // Unknown keystroke, ignore
+ resetParser();
+ 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
+ events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'B':
+ // Down
+ events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'C':
+ // Right
+ events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'D':
+ // Left
+ events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'H':
+ // Home
+ events.add(new TKeypressEvent(kbHome));
+ resetParser();
+ return;
+ case 'F':
+ // End
+ events.add(new TKeypressEvent(kbEnd));
+ resetParser();
+ return;
+ case 'Z':
+ // CBT - Cursor backward X tab stops (default 1)
+ events.add(new TKeypressEvent(kbBackTab));
+ resetParser();
+ return;
+ case 'M':
+ // Mouse position
+ state = ParseState.MOUSE;
+ return;
+ case '<':
+ // Mouse position, SGR (1006) coordinates
+ state = ParseState.MOUSE_SGR;
+ return;
+ case '?':
+ // DEC private mode flag
+ decPrivateModeFlag = true;
+ return;
+ default:
+ break;
+ }
+ }
+
+ // Unknown keystroke, ignore
+ resetParser();
+ 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);
+ }
+ resetParser();
+ return;
+ case 'm':
+ // Generate a mouse release event
+ event = parseMouseSGR(true);
+ if (event != null) {
+ events.add(event);
+ }
+ resetParser();
+ return;
+ default:
+ break;
+ }
+
+ // Unknown keystroke, ignore
+ resetParser();
+ 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());
+ resetParser();
+ return;
+ }
+
+ if ((ch >= 0x30) && (ch <= 0x7E)) {
+ switch (ch) {
+ case 'A':
+ // Up
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'B':
+ // Down
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'C':
+ // Right
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
+ resetParser();
+ return;
+ case 'D':
+ // Left
+ if (params.size() > 1) {
+ shift = csiIsShift(params.get(1));
+ alt = csiIsAlt(params.get(1));
+ ctrl = csiIsCtrl(params.get(1));
+ }
+ events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
+ resetParser();
+ 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));
+ resetParser();
+ 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));
+ resetParser();
+ return;
+ case 'c':
+ // Device Attributes
+ if (decPrivateModeFlag == false) {
+ break;
+ }
+ boolean jexerImages = false;
+ for (String x: params) {
+ if (x.equals("4")) {
+ // Terminal reports sixel support
+ if (debugToStderr) {
+ System.err.println("Device Attributes: sixel");
+ }
+ }
+ if (x.equals("444")) {
+ // Terminal reports Jexer images support
+ if (debugToStderr) {
+ System.err.println("Device Attributes: Jexer images");
+ }
+ jexerImages = true;
+ }
+ }
+ if (jexerImages == false) {
+ // Terminal does not support Jexer images, disable
+ // them.
+ jexerImageOption = JexerImageOption.DISABLED;
+ }
+ return;
+ case 't':
+ // windowOps
+ if ((params.size() > 2) && (params.get(0).equals("4"))) {
+ if (debugToStderr) {
+ System.err.printf("windowOp pixels: " +
+ "height %s width %s\n",
+ params.get(1), params.get(2));
+ }
+ try {
+ widthPixels = Integer.parseInt(params.get(2));
+ heightPixels = Integer.parseInt(params.get(1));
+ } catch (NumberFormatException e) {
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
+ }
+ if (widthPixels <= 0) {
+ widthPixels = 640;
+ }
+ if (heightPixels <= 0) {
+ heightPixels = 400;
+ }
+ }
+ if ((params.size() > 2) && (params.get(0).equals("6"))) {
+ if (debugToStderr) {
+ System.err.printf("windowOp text cell pixels: " +
+ "height %s width %s\n",
+ params.get(1), params.get(2));
+ }
+ try {
+ widthPixels = width * Integer.parseInt(params.get(2));
+ heightPixels = height * Integer.parseInt(params.get(1));
+ } catch (NumberFormatException e) {
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
+ }
+ if (widthPixels <= 0) {
+ widthPixels = 640;
+ }
+ if (heightPixels <= 0) {
+ heightPixels = 400;
+ }
+ }
+ resetParser();
+ return;
+ default:
+ break;
+ }
+ }