Support for Xterm ctrl/alt/shift function keys
[fanfix.git] / src / jexer / io / ECMA48Terminal.java
index ca42db470f25b67749ae715a1fb1536fa39cddfe..069c143f2d32e9a8c6f86dc474ac2149df6f2244 100644 (file)
@@ -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) 2016 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
@@ -106,7 +104,6 @@ public final class ECMA48Terminal implements Runnable {
         ESCAPE_INTERMEDIATE,
         CSI_ENTRY,
         CSI_PARAM,
-        // CSI_INTERMEDIATE,
         MOUSE,
         MOUSE_SGR,
     }
@@ -308,9 +305,9 @@ public final 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) {
@@ -444,36 +441,16 @@ public final 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) {
@@ -779,6 +756,51 @@ public final class ECMA48Terminal implements Runnable {
         }
     }
 
+    /**
+     * 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.
@@ -906,65 +928,21 @@ public final 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;
@@ -1063,15 +1041,9 @@ public final 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();
@@ -1079,15 +1051,9 @@ public final 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();
@@ -1095,15 +1061,9 @@ public final 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();
@@ -1111,19 +1071,33 @@ public final 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;
                 }
@@ -1165,25 +1139,6 @@ public final class ECMA48Terminal implements Runnable {
         return "\033[?1036l";
     }
 
-    /**
-     * 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.
-     *
-     * @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"
-     */
-    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.  Note
      * package private access.
@@ -1332,20 +1287,6 @@ public final class ECMA48Terminal implements Runnable {
         return sb.toString();
     }
 
-    /**
-     * 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"
-     */
-    private String reverse(final boolean on) {
-        if (on) {
-            return "\033[7m";
-        }
-        return "\033[27m";
-    }
-
     /**
      * Create a SGR parameter sequence to reset to defaults.  Note package
      * private access.
@@ -1372,87 +1313,6 @@ public final class ECMA48Terminal implements Runnable {
         return "0;37;40";
     }
 
-    /**
-     * 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"
-     */
-    private 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"
-     */
-    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;";
-    }
-
-    /**
-     * 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"
-     */
-    private 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"
-     */
-    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;";
-    }
-
-    /**
-     * 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"
-     */
-    private String underline(final boolean on) {
-        if (on) {
-            return "\033[4m";
-        }
-        return "\033[24m";
-    }
-
     /**
      * Create a SGR parameter sequence for enabling the visible cursor.  Note
      * package private access.
@@ -1493,35 +1353,6 @@ public final class ECMA48Terminal implements Runnable {
         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
-     */
-    private 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
-     */
-    private 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
-     */
-    private String home() {
-        return "\033[H";
-    }
-
     /**
      * Move the cursor to (x, y).  Note package private access.
      *
@@ -1549,9 +1380,9 @@ public final class ECMA48Terminal implements Runnable {
      */
     private String mouse(final boolean on) {
         if (on) {
-            return "\033[?1003;1005;1006h\033[?1049h";
+            return "\033[?1002;1003;1005;1006h\033[?1049h";
         }
-        return "\033[?1003;1006;1005l\033[?1049l";
+        return "\033[?1002;1003;1006;1005l\033[?1049l";
     }
 
     /**
@@ -1584,17 +1415,18 @@ public final 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);
-                                }
-                                synchronized (listener) {
-                                    listener.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 {