retrofit
[nikiroo-utils.git] / src / jexer / tterminal / ECMA48.java
index c393ca50d46b3f5bd1909ff78e2d312f1f3d8cb7..33a35ca96f61c8e70631a5d4c6a72407aae5e955 100644 (file)
@@ -28,7 +28,7 @@
  */
 package jexer.tterminal;
 
-import java.awt.Graphics2D;
+import java.awt.Graphics;
 import java.awt.image.BufferedImage;
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -274,7 +274,7 @@ public class ECMA48 implements Runnable {
     /**
      * The maximum number of lines in the scrollback buffer.
      */
-    private int maxScrollback = 10000;
+    private int scrollbackMax = 10000;
 
     /**
      * The terminal's input.  For type == XTERM, this is an InputStreamReader
@@ -326,29 +326,29 @@ public class ECMA48 implements Runnable {
      * Physical display width.  We start at 80x24, but the user can resize us
      * bigger/smaller.
      */
-    private int width;
+    private int width = 80;
 
     /**
      * Physical display height.  We start at 80x24, but the user can resize
      * us bigger/smaller.
      */
-    private int height;
+    private int height = 24;
 
     /**
      * Top margin of the scrolling region.
      */
-    private int scrollRegionTop;
+    private int scrollRegionTop = 0;
 
     /**
      * Bottom margin of the scrolling region.
      */
-    private int scrollRegionBottom;
+    private int scrollRegionBottom = height - 1;
 
     /**
      * Right margin column number.  This can be selected by the remote side
      * to be 80/132 (rightMargin values 79/131), or it can be (width - 1).
      */
-    private int rightMargin;
+    private int rightMargin = 79;
 
     /**
      * Last character printed.
@@ -360,7 +360,7 @@ public class ECMA48 implements Runnable {
      * 132), but the line does NOT wrap until another character is written to
      * column 1 of the next line, after which the cursor moves to column 2.
      */
-    private boolean wrapLineFlag;
+    private boolean wrapLineFlag = false;
 
     /**
      * VT220 single shift flag.
@@ -506,6 +506,11 @@ public class ECMA48 implements Runnable {
      */
     private ArrayList<TInputEvent> userQueue = new ArrayList<TInputEvent>();
 
+    /**
+     * Number of bytes/characters passed to consume().
+     */
+    private long readCount = 0;
+
     /**
      * DECSC/DECRC save/restore a subset of the total state.  This class
      * encapsulates those specific flags/modes.
@@ -673,6 +678,8 @@ public class ECMA48 implements Runnable {
         for (int i = 0; i < height; i++) {
             display.add(new DisplayLine(currentState.attr));
         }
+        assert (currentState.cursorY < height);
+        assert (currentState.cursorX < width);
 
         // Spin up the input reader
         readerThread = new Thread(this);
@@ -852,6 +859,34 @@ public class ECMA48 implements Runnable {
     // ECMA48 -----------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Wait for a period of time to get output from the launched process.
+     *
+     * @param millis millis to wait for, or 0 to wait forever
+     * @return true if the launched process has emitted something
+     */
+    public boolean waitForOutput(final int millis) {
+        if (millis < 0) {
+            throw new IllegalArgumentException("timeout must be >= 0");
+        }
+        int waitedMillis = millis;
+        final int pollTimeout = 5;
+        while (true) {
+            if (readCount != 0) {
+                return true;
+            }
+            if ((millis > 0) && (waitedMillis < 0)){
+                return false;
+            }
+            try {
+                Thread.sleep(pollTimeout);
+            } catch (InterruptedException e) {
+                // SQUASH
+            }
+            waitedMillis -= pollTimeout;
+        }
+    }
+
     /**
      * Process keyboard and mouse events from the user.
      *
@@ -1023,11 +1058,6 @@ public class ECMA48 implements Runnable {
         // the input streams.
         if (stopReaderThread == false) {
             stopReaderThread = true;
-            try {
-                readerThread.join(1000);
-            } catch (InterruptedException e) {
-                // SQUASH
-            }
         }
 
         // Now close the output stream.
@@ -1207,8 +1237,8 @@ public class ECMA48 implements Runnable {
         int delta = height - this.height;
         this.height = height;
         scrollRegionBottom += delta;
-        if (scrollRegionBottom < 0) {
-            scrollRegionBottom = height;
+        if ((scrollRegionBottom < 0) || (scrollRegionTop > height - 1)) {
+            scrollRegionBottom = height - 1;
         }
         if (scrollRegionTop >= scrollRegionBottom) {
             scrollRegionTop = 0;
@@ -1225,10 +1255,29 @@ public class ECMA48 implements Runnable {
             display.add(line);
         }
         while (display.size() > height) {
-            scrollback.add(display.remove(0));
+            appendScrollbackLine(display.remove(0));
         }
     }
 
+    /**
+     * Get the maximum number of lines in the scrollback buffer.
+     *
+     * @return the maximum number of lines in the scrollback buffer
+     */
+    public int getScrollbackMax() {
+        return scrollbackMax;
+    }
+
+    /**
+     * Set the maximum number of lines for the scrollback buffer.
+     *
+     * @param scrollbackMax the maximum number of lines for the scrollback
+     * buffer
+     */
+    public final void setScrollbackMax(final int scrollbackMax) {
+        this.scrollbackMax = scrollbackMax;
+    }
+
     /**
      * Get visible cursor flag.
      *
@@ -1378,8 +1427,13 @@ public class ECMA48 implements Runnable {
         currentState            = new SaveableState();
         savedState              = new SaveableState();
         scanState               = ScanState.GROUND;
-        width                   = 80;
-        height                  = 24;
+        if (displayListener != null) {
+            width = displayListener.getDisplayWidth();
+            height = displayListener.getDisplayHeight();
+        } else {
+            width               = 80;
+            height              = 24;
+        }
         scrollRegionTop         = 0;
         scrollRegionBottom      = height - 1;
         rightMargin             = width - 1;
@@ -1387,11 +1441,6 @@ public class ECMA48 implements Runnable {
         arrowKeyMode            = ArrowKeyMode.ANSI;
         keypadMode              = KeypadMode.Numeric;
         wrapLineFlag            = false;
-        if (displayListener != null) {
-            width = displayListener.getDisplayWidth();
-            height = displayListener.getDisplayHeight();
-            rightMargin         = width - 1;
-        }
 
         // Flags
         shiftOut                = false;
@@ -1422,14 +1471,25 @@ public class ECMA48 implements Runnable {
         toGround();
     }
 
+    /**
+     * Append a to the scrollback buffer, clearing image data for lines more
+     * than three screenfuls in.
+     */
+    private void appendScrollbackLine(DisplayLine line) {
+        scrollback.add(line);
+        if (scrollback.size() > height * 3) {
+            scrollback.get(scrollback.size() - (height * 3)).clearImages();
+        }
+    }
+
     /**
      * Append a new line to the bottom of the display, adding lines off the
      * top to the scrollback buffer.
      */
     private void newDisplayLine() {
         // Scroll the top line off into the scrollback buffer
-        scrollback.add(display.get(0));
-        if (scrollback.size() > maxScrollback) {
+        appendScrollbackLine(display.get(0));
+        while (scrollback.size() > scrollbackMax) {
             scrollback.remove(0);
             scrollback.trimToSize();
         }
@@ -1474,7 +1534,6 @@ public class ECMA48 implements Runnable {
      * Handle a linefeed.
      */
     private void linefeed() {
-
         if (currentState.cursorY < scrollRegionBottom) {
             // Increment screen y
             currentState.cursorY++;
@@ -1685,35 +1744,45 @@ public class ECMA48 implements Runnable {
         if (mouseEncoding == MouseEncoding.SGR) {
             sb.append((char) 0x1B);
             sb.append("[<");
+            int buttons = 0;
 
             if (mouse.isMouse1()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("32;");
+                    buttons = 32;
                 } else {
-                    sb.append("0;");
+                    buttons = 0;
                 }
             } else if (mouse.isMouse2()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("33;");
+                    buttons = 33;
                 } else {
-                    sb.append("1;");
+                    buttons = 1;
                 }
             } else if (mouse.isMouse3()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("34;");
+                    buttons = 34;
                 } else {
-                    sb.append("2;");
+                    buttons = 2;
                 }
             } else if (mouse.isMouseWheelUp()) {
-                sb.append("64;");
+                buttons = 64;
             } else if (mouse.isMouseWheelDown()) {
-                sb.append("65;");
+                buttons = 65;
             } else {
                 // This is motion with no buttons down.
-                sb.append("35;");
+                buttons = 35;
+            }
+            if (mouse.isAlt()) {
+                buttons |= 0x08;
+            }
+            if (mouse.isCtrl()) {
+                buttons |= 0x10;
+            }
+            if (mouse.isShift()) {
+                buttons |= 0x04;
             }
 
-            sb.append(String.format("%d;%d", mouse.getX() + 1,
+            sb.append(String.format("%d;%d;%d", buttons, mouse.getX() + 1,
                     mouse.getY() + 1));
 
             if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
@@ -1727,35 +1796,46 @@ public class ECMA48 implements Runnable {
             sb.append((char) 0x1B);
             sb.append('[');
             sb.append('M');
+            int buttons = 0;
             if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
-                sb.append((char) (0x03 + 32));
+                buttons = 0x03 + 32;
             } else if (mouse.isMouse1()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x00 + 32 + 32));
+                    buttons = 0x00 + 32 + 32;
                 } else {
-                    sb.append((char) (0x00 + 32));
+                    buttons = 0x00 + 32;
                 }
             } else if (mouse.isMouse2()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x01 + 32 + 32));
+                    buttons = 0x01 + 32 + 32;
                 } else {
-                    sb.append((char) (0x01 + 32));
+                    buttons = 0x01 + 32;
                 }
             } else if (mouse.isMouse3()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x02 + 32 + 32));
+                    buttons = 0x02 + 32 + 32;
                 } else {
-                    sb.append((char) (0x02 + 32));
+                    buttons = 0x02 + 32;
                 }
             } else if (mouse.isMouseWheelUp()) {
-                sb.append((char) (0x04 + 64));
+                buttons = 0x04 + 64;
             } else if (mouse.isMouseWheelDown()) {
-                sb.append((char) (0x05 + 64));
+                buttons = 0x05 + 64;
             } else {
                 // This is motion with no buttons down.
-                sb.append((char) (0x03 + 32));
+                buttons = 0x03 + 32;
+            }
+            if (mouse.isAlt()) {
+                buttons |= 0x08;
+            }
+            if (mouse.isCtrl()) {
+                buttons |= 0x10;
+            }
+            if (mouse.isShift()) {
+                buttons |= 0x04;
             }
 
+            sb.append((char) (buttons & 0xFF));
             sb.append((char) (mouse.getX() + 33));
             sb.append((char) (mouse.getY() + 33));
         }
@@ -3177,10 +3257,10 @@ public class ECMA48 implements Runnable {
                     if (decPrivateModeFlag == true) {
                         if (value == true) {
                             // Enable sixel scrolling (default).
-                            // TODO
+                            // Not supported
                         } else {
                             // Disable sixel scrolling.
-                            // TODO
+                            // Not supported
                         }
                     }
                 }
@@ -4015,7 +4095,7 @@ public class ECMA48 implements Runnable {
 
                 case 8:
                     // Invisible
-                    // TODO
+                    // Not supported
                     break;
 
                 case 90:
@@ -4400,6 +4480,9 @@ public class ECMA48 implements Runnable {
             // DECSTBM
             int top = getCsiParam(0, 1, 1, height) - 1;
             int bottom = getCsiParam(1, height, 1, height) - 1;
+            if (bottom > height - 1) {
+                bottom = height - 1;
+            }
 
             if (top > bottom) {
                 top = bottom;
@@ -4753,13 +4836,22 @@ public class ECMA48 implements Runnable {
     private void oscPut(final char xtermChar) {
         // System.err.println("oscPut: " + xtermChar);
 
+        boolean oscEnd = false;
+
+        if (xtermChar == 0x07) {
+            oscEnd = true;
+        }
+        if ((xtermChar == '\\')
+            && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033')
+        ) {
+            oscEnd = true;
+        }
+
         // Collect first
         collectBuffer.append(xtermChar);
 
         // Xterm cases...
-        if ((xtermChar == 0x07)
-            || (collectBuffer.toString().endsWith("\033\\"))
-        ) {
+        if (oscEnd) {
             String args = null;
             if (xtermChar == 0x07) {
                 args = collectBuffer.substring(0, collectBuffer.length() - 1);
@@ -4842,11 +4934,19 @@ public class ECMA48 implements Runnable {
     private void pmPut(final char pmChar) {
         // System.err.println("pmPut: " + pmChar);
 
+        boolean pmEnd = false;
+
+        if ((pmChar == '\\')
+            && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033')
+        ) {
+            pmEnd = true;
+        }
+
         // Collect first
         collectBuffer.append(pmChar);
 
         // Xterm cases...
-        if (collectBuffer.toString().endsWith("\033\\")) {
+        if (pmEnd) {
             String arg = null;
             arg = collectBuffer.substring(0, collectBuffer.length() - 2);
 
@@ -4934,6 +5034,7 @@ public class ECMA48 implements Runnable {
      * @param ch character from the remote side
      */
     private void consume(final int ch) {
+        readCount++;
 
         // DEBUG
         // System.err.printf("%c STATE = %s\n", ch, scanState);
@@ -7107,8 +7208,7 @@ public class ECMA48 implements Runnable {
             return;
         }
 
-        java.util.Base64.Decoder base64 = java.util.Base64.getDecoder();
-        byte [] bytes = base64.decode(data);
+        byte [] bytes = StringUtils.fromBase64(data.getBytes());
         if (bytes.length != (imageWidth * imageHeight * 3)) {
             return;
         }
@@ -7154,8 +7254,7 @@ public class ECMA48 implements Runnable {
         boolean scroll = false;
         BufferedImage image = null;
         try {
-            java.util.Base64.Decoder base64 = java.util.Base64.getDecoder();
-            byte [] bytes = base64.decode(data);
+            byte [] bytes = StringUtils.fromBase64(data.getBytes());
 
             switch (type) {
             case 1:
@@ -7270,8 +7369,21 @@ public class ECMA48 implements Runnable {
                 }
 
                 Cell cell = new Cell();
-                cell.setImage(image.getSubimage(x * textWidth,
-                        y * textHeight, width, height));
+                if ((width != textWidth) || (height != textHeight)) {
+                    BufferedImage newImage;
+                    newImage = new BufferedImage(textWidth, textHeight,
+                        BufferedImage.TYPE_INT_ARGB);
+
+                    Graphics gr = newImage.getGraphics();
+                    gr.drawImage(image.getSubimage(x * textWidth,
+                            y * textHeight, width, height),
+                        0, 0, null, null);
+                    gr.dispose();
+                    cell.setImage(newImage);
+                } else {
+                    cell.setImage(image.getSubimage(x * textWidth,
+                            y * textHeight, width, height));
+                }
 
                 cells[x][y] = cell;
             }
@@ -7283,9 +7395,10 @@ public class ECMA48 implements Runnable {
             for (int x = 0; x < cellColumns; x++) {
                 assert (currentState.cursorX <= rightMargin);
 
-                // TODO: Render text of current cell first, then image over
-                // it (accounting for blank pixels).  For now, just copy the
-                // cell.
+                // A real sixel terminal would render the text of the current
+                // cell first, then image over it (accounting for blank
+                // pixels).  We do not support that.  A cell is either text,
+                // or image, but not a mix of image-over-text.
                 DisplayLine line = display.get(currentState.cursorY);
                 line.replace(currentState.cursorX, cells[x][y]);
 
@@ -7306,6 +7419,7 @@ public class ECMA48 implements Runnable {
                 // At the bottom, no more scrolling, done.
                 break;
             }
+
             cursorPosition(currentState.cursorY, x0);
         }