#35 support emoji in tterminal
[fanfix.git] / src / jexer / tterminal / ECMA48.java
index f00164ba869b5f32d1a282889693828428c15464..9ff37a181fdfc0871e086d47782a1c94f7852904 100644 (file)
@@ -52,6 +52,8 @@ import jexer.bits.Color;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
 import jexer.bits.StringUtils;
+import jexer.event.TInputEvent;
+import jexer.event.TKeypressEvent;
 import jexer.event.TMouseEvent;
 import jexer.io.ReadTimeoutException;
 import jexer.io.TimeoutInputStream;
@@ -348,7 +350,7 @@ public class ECMA48 implements Runnable {
     /**
      * Last character printed.
      */
-    private char repCh;
+    private int repCh;
 
     /**
      * VT100-style line wrapping: a character is placed in column 80 (or
@@ -488,7 +490,13 @@ public class ECMA48 implements Runnable {
     /**
      * The glyph drawer for full-width chars.
      */
-    GlyphMaker glyphMaker = null;
+    private GlyphMaker glyphMaker = null;
+
+    /**
+     * Input queue for keystrokes and mouse events to send to the remote
+     * side.
+     */
+    private ArrayList<TInputEvent> userQueue = new ArrayList<TInputEvent>();
 
     /**
      * DECSC/DECRC save/restore a subset of the total state.  This class
@@ -682,12 +690,18 @@ public class ECMA48 implements Runnable {
         char [] readBufferUTF8 = null;
         byte [] readBuffer = null;
         if (utf8) {
-            readBufferUTF8 = new char[128];
+            readBufferUTF8 = new char[2048];
         } else {
-            readBuffer = new byte[128];
+            readBuffer = new byte[2048];
         }
 
         while (!done && !stopReaderThread) {
+            synchronized (userQueue) {
+                while (userQueue.size() > 0) {
+                    handleUserEvent(userQueue.remove(0));
+                }
+            }
+
             try {
                 int n = inputStream.available();
 
@@ -709,7 +723,7 @@ public class ECMA48 implements Runnable {
                 }
                 if (n == 0) {
                     try {
-                        Thread.sleep(2);
+                        Thread.sleep(10);
                     } catch (InterruptedException e) {
                         // SQUASH
                     }
@@ -736,15 +750,17 @@ public class ECMA48 implements Runnable {
                 } else {
                     // Don't step on UI events
                     synchronized (this) {
-                        for (int i = 0; i < rc; i++) {
-                            int ch = 0;
-                            if (utf8) {
-                                ch = readBufferUTF8[i];
-                            } else {
-                                ch = readBuffer[i];
+                        if (utf8) {
+                            for (int i = 0; i < rc;) {
+                                int ch = Character.codePointAt(readBufferUTF8,
+                                    i);
+                                i += Character.charCount(ch);
+                                consume(ch);
+                            }
+                        } else {
+                            for (int i = 0; i < rc; i++) {
+                                consume(readBuffer[i]);
                             }
-
-                            consume((char) ch);
                         }
                     }
                     // Permit my enclosing UI to know that I updated.
@@ -810,6 +826,31 @@ public class ECMA48 implements Runnable {
     // ECMA48 -----------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Process keyboard and mouse events from the user.
+     *
+     * @param event the input event to consume
+     */
+    private void handleUserEvent(final TInputEvent event) {
+        if (event instanceof TKeypressEvent) {
+            keypress(((TKeypressEvent) event).getKey());
+        }
+        if (event instanceof TMouseEvent) {
+            mouse((TMouseEvent) event);
+        }
+    }
+
+    /**
+     * Add a keyboard and mouse event from the user to the queue.
+     *
+     * @param event the input event to consume
+     */
+    public void addUserEvent(final TInputEvent event) {
+        synchronized (userQueue) {
+            userQueue.add(event);
+        }
+    }
+
     /**
      * Return the proper primary Device Attributes string.
      *
@@ -1387,7 +1428,7 @@ public class ECMA48 implements Runnable {
      *
      * @param ch character to display
      */
-    private void printCharacter(final char ch) {
+    private void printCharacter(final int ch) {
         int rightMargin = this.rightMargin;
 
         if (StringUtils.width(ch) == 2) {
@@ -1495,7 +1536,7 @@ public class ECMA48 implements Runnable {
      *
      * @param mouse mouse event received from the local user
      */
-    public void mouse(final TMouseEvent mouse) {
+    private void mouse(final TMouseEvent mouse) {
 
         /*
         System.err.printf("mouse(): protocol %s encoding %s mouse %s\n",
@@ -1643,7 +1684,7 @@ public class ECMA48 implements Runnable {
      *
      * @param keypress keypress received from the local user
      */
-    public void keypress(final TKeypress keypress) {
+    private void keypress(final TKeypress keypress) {
         writeRemote(keypressToString(keypress));
     }
 
@@ -1702,7 +1743,7 @@ public class ECMA48 implements Runnable {
              * the remote side.
              */
             if (keypress.getChar() < 0x20) {
-                handleControlChar(keypress.getChar());
+                handleControlChar((char) keypress.getChar());
             } else {
                 // Local echo for everything else
                 printCharacter(keypress.getChar());
@@ -1717,17 +1758,17 @@ public class ECMA48 implements Runnable {
         // Handle control characters
         if ((keypress.isCtrl()) && (!keypress.isFnKey())) {
             StringBuilder sb = new StringBuilder();
-            char ch = keypress.getChar();
+            int ch = keypress.getChar();
             ch -= 0x40;
-            sb.append(ch);
+            sb.append(Character.toChars(ch));
             return sb.toString();
         }
 
         // Handle alt characters
         if ((keypress.isAlt()) && (!keypress.isFnKey())) {
             StringBuilder sb = new StringBuilder("\033");
-            char ch = keypress.getChar();
-            sb.append(ch);
+            int ch = keypress.getChar();
+            sb.append(Character.toChars(ch));
             return sb.toString();
         }
 
@@ -2279,7 +2320,7 @@ public class ECMA48 implements Runnable {
         // Non-alt, non-ctrl characters
         if (!keypress.isFnKey()) {
             StringBuilder sb = new StringBuilder();
-            sb.append(keypress.getChar());
+            sb.append(Character.toChars(keypress.getChar()));
             return sb.toString();
         }
         return "";
@@ -2294,7 +2335,7 @@ public class ECMA48 implements Runnable {
      * @param charsetGr character set defined for GR
      * @return character to display on the screen
      */
-    private char mapCharacterCharset(final char ch,
+    private char mapCharacterCharset(final int ch,
         final CharacterSet charsetGl,
         final CharacterSet charsetGr) {
 
@@ -2372,7 +2413,7 @@ public class ECMA48 implements Runnable {
      * @param ch either 8-bit or Unicode character from the remote side
      * @return character to display on the screen
      */
-    private char mapCharacter(final char ch) {
+    private int mapCharacter(final int ch) {
         if (ch >= 0x100) {
             // Unicode character, just return it
             return ch;
@@ -3419,8 +3460,7 @@ public class ECMA48 implements Runnable {
      * DECALN - Screen alignment display.
      */
     private void decaln() {
-        Cell newCell = new Cell();
-        newCell.setChar('E');
+        Cell newCell = new Cell('E');
         for (DisplayLine line: display) {
             for (int i = 0; i < line.length(); i++) {
                 line.replace(i, newCell);
@@ -4694,14 +4734,14 @@ public class ECMA48 implements Runnable {
      *
      * @param ch character from the remote side
      */
-    private void consume(char ch) {
+    private void consume(int ch) {
 
         // DEBUG
         // System.err.printf("%c STATE = %s\n", ch, scanState);
 
         // Special case for VT10x: 7-bit characters only
         if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) {
-            ch = (char)(ch & 0x7F);
+            ch = (ch & 0x7F);
         }
 
         // Special "anywhere" states
@@ -4771,7 +4811,7 @@ public class ECMA48 implements Runnable {
             // 00-17, 19, 1C-1F --> execute
             // 80-8F, 91-9A, 9C --> execute
             if ((ch <= 0x1F) || ((ch >= 0x80) && (ch <= 0x9F))) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-7F            --> print
@@ -4798,13 +4838,13 @@ public class ECMA48 implements Runnable {
         case ESCAPE:
             // 00-17, 19, 1C-1F --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
                 return;
             }
 
             // 20-2F            --> collect, then switch to ESCAPE_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.ESCAPE_INTERMEDIATE;
                 return;
             }
@@ -5140,12 +5180,12 @@ public class ECMA48 implements Runnable {
         case ESCAPE_INTERMEDIATE:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
             }
 
             // 30-7E               --> dispatch, then switch to GROUND
@@ -5747,12 +5787,12 @@ public class ECMA48 implements Runnable {
         case CSI_ENTRY:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect, then switch to CSI_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.CSI_INTERMEDIATE;
             }
 
@@ -5768,7 +5808,7 @@ public class ECMA48 implements Runnable {
 
             // 3C-3F               --> collect, then switch to CSI_PARAM
             if ((ch >= 0x3C) && (ch <= 0x3F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.CSI_PARAM;
             }
 
@@ -6020,12 +6060,12 @@ public class ECMA48 implements Runnable {
         case CSI_PARAM:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect, then switch to CSI_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.CSI_INTERMEDIATE;
             }
 
@@ -6274,12 +6314,12 @@ public class ECMA48 implements Runnable {
         case CSI_INTERMEDIATE:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
             }
 
             // 0x30-3F goes to CSI_IGNORE
@@ -6387,12 +6427,12 @@ public class ECMA48 implements Runnable {
         case CSI_IGNORE:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
             }
 
             // 40-7E               --> ignore, then switch to GROUND
@@ -6413,7 +6453,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6425,7 +6465,7 @@ public class ECMA48 implements Runnable {
 
             // 20-2F               --> collect, then switch to DCS_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.DCS_INTERMEDIATE;
             }
 
@@ -6441,7 +6481,7 @@ public class ECMA48 implements Runnable {
 
             // 3C-3F               --> collect, then switch to DCS_PARAM
             if ((ch >= 0x3C) && (ch <= 0x3F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.DCS_PARAM;
             }
 
@@ -6471,7 +6511,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6503,7 +6543,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6515,7 +6555,7 @@ public class ECMA48 implements Runnable {
 
             // 20-2F          --> collect, then switch to DCS_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.DCS_INTERMEDIATE;
             }
 
@@ -6555,7 +6595,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6603,7 +6643,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6641,11 +6681,11 @@ public class ECMA48 implements Runnable {
 
             // Special case for Jexer: PM can pass one control character
             if (ch == 0x1B) {
-                pmPut(ch);
+                pmPut((char) ch);
             }
 
             if ((ch >= 0x20) && (ch <= 0x7F)) {
-                pmPut(ch);
+                pmPut((char) ch);
             }
 
             // 0x9C goes to GROUND
@@ -6658,14 +6698,14 @@ public class ECMA48 implements Runnable {
         case OSC_STRING:
             // Special case for Xterm: OSC can pass control characters
             if ((ch == 0x9C) || (ch == 0x07) || (ch == 0x1B)) {
-                oscPut(ch);
+                oscPut((char) ch);
             }
 
             // 00-17, 19, 1C-1F        --> ignore
 
             // 20-7F                   --> osc_put
             if ((ch >= 0x20) && (ch <= 0x7F)) {
-                oscPut(ch);
+                oscPut((char) ch);
             }
 
             // 0x9C goes to GROUND
@@ -6678,7 +6718,7 @@ public class ECMA48 implements Runnable {
         case VT52_DIRECT_CURSOR_ADDRESS:
             // This is a special case for the VT52 sequence "ESC Y l c"
             if (collectBuffer.length() == 0) {
-                collect(ch);
+                collect((char) ch);
             } else if (collectBuffer.length() == 1) {
                 // We've got the two characters, one in the buffer and the
                 // other in ch.
@@ -6861,7 +6901,7 @@ public class ECMA48 implements Runnable {
      * @param ch the character to draw
      */
     private void drawHalves(final int leftX, final int leftY,
-        final int rightX, final int rightY, final char ch) {
+        final int rightX, final int rightY, final int ch) {
 
         // System.err.println("drawHalves(): " + Integer.toHexString(ch));
 
@@ -6870,8 +6910,7 @@ public class ECMA48 implements Runnable {
             lastTextHeight = textHeight;
         }
 
-        Cell cell = new Cell(ch);
-        cell.setAttr(currentState.attr);
+        Cell cell = new Cell(ch, currentState.attr);
         BufferedImage image = glyphMaker.getImage(cell, textWidth * 2,
             textHeight);
         BufferedImage leftImage = image.getSubimage(0, 0, textWidth,
@@ -6879,14 +6918,12 @@ public class ECMA48 implements Runnable {
         BufferedImage rightImage = image.getSubimage(textWidth, 0, textWidth,
             textHeight);
 
-        Cell left = new Cell();
-        left.setTo(cell);
+        Cell left = new Cell(cell);
         left.setImage(leftImage);
         left.setWidth(Cell.Width.LEFT);
         display.get(leftY).replace(leftX, left);
 
-        Cell right = new Cell();
-        right.setTo(cell);
+        Cell right = new Cell(cell);
         right.setImage(rightImage);
         right.setWidth(Cell.Width.RIGHT);
         display.get(rightY).replace(rightX, right);