headless backend demo
[fanfix.git] / src / jexer / backend / ECMA48Terminal.java
index 613a3abd8f8f2feee14022645469019b6c597201..0389f3d388e3146c64aeab299c5f2ec357110a70 100644 (file)
@@ -51,6 +51,7 @@ import jexer.TImage;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
 import jexer.bits.Color;
+import jexer.bits.StringUtils;
 import jexer.event.TCommandEvent;
 import jexer.event.TInputEvent;
 import jexer.event.TKeypressEvent;
@@ -199,6 +200,11 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private boolean sixel = true;
 
+    /**
+     * If true, use a single shared palette for sixel.
+     */
+    private boolean sixelSharedPalette = true;
+
     /**
      * The sixel palette handler.
      */
@@ -237,11 +243,6 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private ImageCache jexerCache = null;
 
-    /**
-     * Base64 encoder used by iTerm2 and Jexer images.
-     */
-    private java.util.Base64.Encoder base64 = null;
-
     /**
      * If true, then we changed System.in and need to change it back.
      */
@@ -1167,11 +1168,12 @@ public class ECMA48Terminal extends LogicalScreen
 
         // Enable mouse reporting and metaSendsEscape
         this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
-        this.output.flush();
 
         // Request xterm use the sixel settings we want
         this.output.printf("%s", xtermSetSixelSettings());
 
+        this.output.flush();
+
         // Query the screen size
         sessionInfo.queryWindowSize();
         setDimensions(sessionInfo.getWindowWidth(),
@@ -1259,11 +1261,12 @@ public class ECMA48Terminal extends LogicalScreen
 
         // Enable mouse reporting and metaSendsEscape
         this.output.printf("%s%s", mouse(true), xtermMetaSendsEscape(true));
-        this.output.flush();
 
         // Request xterm use the sixel settings we want
         this.output.printf("%s", xtermSetSixelSettings());
 
+        this.output.flush();
+
         // Query the screen size
         sessionInfo.queryWindowSize();
         setDimensions(sessionInfo.getWindowWidth(),
@@ -1490,6 +1493,14 @@ public class ECMA48Terminal extends LogicalScreen
             // SQUASH
         }
 
+        // Shared palette
+        if (System.getProperty("jexer.ECMA48.sixelSharedPalette",
+                "true").equals("false")) {
+            sixelSharedPalette = false;
+        } else {
+            sixelSharedPalette = true;
+        }
+
         // Default to not supporting iTerm2 images.
         if (System.getProperty("jexer.ECMA48.iTerm2Images",
                 "false").equals("true")) {
@@ -1632,7 +1643,10 @@ public class ECMA48Terminal extends LogicalScreen
      * @return the width in pixels of a character cell
      */
     public int getTextWidth() {
-        return (widthPixels / sessionInfo.getWindowWidth());
+        if (sessionInfo.getWindowWidth() > 0) {
+            return (widthPixels / sessionInfo.getWindowWidth());
+        }
+        return 16;
     }
 
     /**
@@ -1641,7 +1655,10 @@ public class ECMA48Terminal extends LogicalScreen
      * @return the height in pixels of a character cell
      */
     public int getTextHeight() {
-        return (heightPixels / sessionInfo.getWindowHeight());
+        if (sessionInfo.getWindowHeight() > 0) {
+            return (heightPixels / sessionInfo.getWindowHeight());
+        }
+        return 20;
     }
 
     /**
@@ -2227,10 +2244,13 @@ public class ECMA48Terminal extends LogicalScreen
         boolean eventMouse3 = false;
         boolean eventMouseWheelUp = false;
         boolean eventMouseWheelDown = false;
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
 
         // System.err.printf("buttons: %04x\r\n", buttons);
 
-        switch (buttons) {
+        switch (buttons & 0xE3) {
         case 0:
             eventMouse1 = true;
             mouse1 = true;
@@ -2312,9 +2332,21 @@ public class ECMA48Terminal extends LogicalScreen
             eventType = TMouseEvent.Type.MOUSE_MOTION;
             break;
         }
+
+        if ((buttons & 0x04) != 0) {
+            eventShift = true;
+        }
+        if ((buttons & 0x08) != 0) {
+            eventAlt = true;
+        }
+        if ((buttons & 0x10) != 0) {
+            eventCtrl = true;
+        }
+
         return new TMouseEvent(eventType, x, y, x, y,
             eventMouse1, eventMouse2, eventMouse3,
-            eventMouseWheelUp, eventMouseWheelDown);
+            eventMouseWheelUp, eventMouseWheelDown,
+            eventAlt, eventCtrl, eventShift);
     }
 
     /**
@@ -2349,12 +2381,15 @@ public class ECMA48Terminal extends LogicalScreen
         boolean eventMouse3 = false;
         boolean eventMouseWheelUp = false;
         boolean eventMouseWheelDown = false;
+        boolean eventAlt = false;
+        boolean eventCtrl = false;
+        boolean eventShift = false;
 
         if (release) {
             eventType = TMouseEvent.Type.MOUSE_UP;
         }
 
-        switch (buttons) {
+        switch (buttons & 0xE3) {
         case 0:
             eventMouse1 = true;
             break;
@@ -2411,9 +2446,21 @@ public class ECMA48Terminal extends LogicalScreen
             // Unknown, bail out
             return null;
         }
+
+        if ((buttons & 0x04) != 0) {
+            eventShift = true;
+        }
+        if ((buttons & 0x08) != 0) {
+            eventAlt = true;
+        }
+        if ((buttons & 0x10) != 0) {
+            eventCtrl = true;
+        }
+
         return new TMouseEvent(eventType, x, y, x, y,
             eventMouse1, eventMouse2, eventMouse3,
-            eventMouseWheelUp, eventMouseWheelDown);
+            eventMouseWheelUp, eventMouseWheelDown,
+            eventAlt, eventCtrl, eventShift);
     }
 
     /**
@@ -2845,12 +2892,20 @@ public class ECMA48Terminal extends LogicalScreen
                             }
                             jexerImages = true;
                         }
+                        if (x.equals("1337")) {
+                            // Terminal reports iTerm2 images support
+                            if (debugToStderr) {
+                                System.err.println("Device Attributes: iTerm2 images");
+                            }
+                            iterm2Images = true;
+                        }
                     }
                     if (jexerImages == false) {
                         // Terminal does not support Jexer images, disable
                         // them.
                         jexerImageOption = JexerImageOption.DISABLED;
                     }
+                    resetParser();
                     return;
                 case 't':
                     // windowOps
@@ -2930,12 +2985,16 @@ public class ECMA48Terminal extends LogicalScreen
      *   - enable sixel scrolling
      *
      *   - disable private color registers (so that we can use one common
-     *     palette)
+     *     palette) if sixelSharedPalette is set
      *
      * @return the string to emit to xterm
      */
     private String xtermSetSixelSettings() {
-        return "\033[?80h\033[?1070l";
+        if (sixelSharedPalette == true) {
+            return "\033[?80h\033[?1070l";
+        } else {
+            return "\033[?80h\033[?1070h";
+        }
     }
 
     /**
@@ -3052,8 +3111,9 @@ public class ECMA48Terminal extends LogicalScreen
 
         if (palette == null) {
             palette = new SixelPalette();
-            // TODO: make this an option (shared palette or not)
-            palette.emitPalette(sb, null);
+            if (sixelSharedPalette == true) {
+                palette.emitPalette(sb, null);
+            }
         }
 
         return sb.toString();
@@ -3101,9 +3161,8 @@ public class ECMA48Terminal extends LogicalScreen
         if (y == height - 1) {
             // We are on the bottom row.  If scrolling mode is enabled
             // (default), then VT320/xterm will scroll the entire screen if
-            // we draw any pixels here.
-
-            // TODO: support sixel scrolling mode disabled as an option.
+            // we draw any pixels here.  Do not draw the image, bail out
+            // instead.
             sb.append(normal());
             sb.append(gotoXY(x, y));
             for (int j = 0; j < cells.size(); j++) {
@@ -3139,12 +3198,10 @@ public class ECMA48Terminal extends LogicalScreen
         int imageWidth = cells.get(0).getImage().getWidth();
         int imageHeight = cells.get(0).getImage().getHeight();
 
-        // cells.get(x).getImage() has a dithered bitmap containing indexes
-        // into the color palette.  Piece these together into one larger
-        // image for final rendering.
+        // Piece these together into one larger image for final rendering.
         int totalWidth = 0;
-        int fullWidth = cells.size() * getTextWidth();
-        int fullHeight = getTextHeight();
+        int fullWidth = cells.size() * imageWidth;
+        int fullHeight = imageHeight;
         for (int i = 0; i < cells.size(); i++) {
             totalWidth += cells.get(i).getImage().getWidth();
         }
@@ -3154,10 +3211,8 @@ public class ECMA48Terminal extends LogicalScreen
 
         int [] rgbArray;
         for (int i = 0; i < cells.size() - 1; i++) {
-            int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
-                imageWidth);
-            int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
-                imageHeight);
+            int tileWidth = imageWidth;
+            int tileHeight = imageHeight;
 
             if (false && cells.get(i).isInvertedImage()) {
                 // I used to put an all-white cell over the cursor, don't do
@@ -3225,7 +3280,7 @@ public class ECMA48Terminal extends LogicalScreen
         image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
             imageHeight, rgbArray, 0, totalWidth);
 
-        if (totalWidth < getTextWidth()) {
+        if (totalWidth < imageWidth) {
             int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
 
             for (int imageX = image.getWidth() - totalWidth;
@@ -3237,11 +3292,28 @@ public class ECMA48Terminal extends LogicalScreen
             }
         }
 
+        if ((image.getWidth() != cells.size() * getTextWidth())
+            || (image.getHeight() != getTextHeight())
+        ) {
+            // Rescale the image to fit the text cells it is going into.
+            BufferedImage newImage;
+            newImage = new BufferedImage(cells.size() * getTextWidth(),
+                getTextHeight(), BufferedImage.TYPE_INT_ARGB);
+
+            java.awt.Graphics gr = newImage.getGraphics();
+            gr.drawImage(image, 0, 0, newImage.getWidth(),
+                newImage.getHeight(), null, null);
+            gr.dispose();
+            image = newImage;
+            fullHeight = image.getHeight();
+        }
+
         // Dither the image.  It is ok to lose the original here.
         if (palette == null) {
             palette = new SixelPalette();
-            // TODO: make this an option (shared palette or not)
-            palette.emitPalette(sb, null);
+            if (sixelSharedPalette == true) {
+                palette.emitPalette(sb, null);
+            }
         }
         image = palette.ditherImage(image);
 
@@ -3249,20 +3321,17 @@ public class ECMA48Terminal extends LogicalScreen
         int rasterHeight = 0;
         int rasterWidth = image.getWidth();
 
-        /*
-
-        // TODO: make this an option (shared palette or not)
-
-        // Emit the palette, but only for the colors actually used by these
-        // cells.
-        boolean [] usedColors = new boolean[sixelPaletteSize];
-        for (int imageX = 0; imageX < image.getWidth(); imageX++) {
-            for (int imageY = 0; imageY < image.getHeight(); imageY++) {
-                usedColors[image.getRGB(imageX, imageY)] = true;
+        if (sixelSharedPalette == false) {
+            // Emit the palette, but only for the colors actually used by
+            // these cells.
+            boolean [] usedColors = new boolean[sixelPaletteSize];
+            for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+                for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+                    usedColors[image.getRGB(imageX, imageY)] = true;
+                }
             }
+            palette.emitPalette(sb, usedColors);
         }
-        palette.emitPalette(sb, usedColors);
-         */
 
         // Render the entire row of cells.
         for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
@@ -3429,7 +3498,6 @@ public class ECMA48Terminal extends LogicalScreen
 
         if (iterm2Cache == null) {
             iterm2Cache = new ImageCache(height * 10);
-            base64 = java.util.Base64.getEncoder();
         }
 
         // Save and get rows to/from the cache that do NOT have inverted
@@ -3457,8 +3525,8 @@ public class ECMA48Terminal extends LogicalScreen
         // Piece cells.get(x).getImage() pieces together into one larger
         // image for final rendering.
         int totalWidth = 0;
-        int fullWidth = cells.size() * getTextWidth();
-        int fullHeight = getTextHeight();
+        int fullWidth = cells.size() * imageWidth;
+        int fullHeight = imageHeight;
         for (int i = 0; i < cells.size(); i++) {
             totalWidth += cells.get(i).getImage().getWidth();
         }
@@ -3468,10 +3536,8 @@ public class ECMA48Terminal extends LogicalScreen
 
         int [] rgbArray;
         for (int i = 0; i < cells.size() - 1; i++) {
-            int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
-                imageWidth);
-            int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
-                imageHeight);
+            int tileWidth = imageWidth;
+            int tileHeight = imageHeight;
             if (false && cells.get(i).isInvertedImage()) {
                 // I used to put an all-white cell over the cursor, don't do
                 // that anymore.
@@ -3538,7 +3604,7 @@ public class ECMA48Terminal extends LogicalScreen
         image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
             imageHeight, rgbArray, 0, totalWidth);
 
-        if (totalWidth < getTextWidth()) {
+        if (totalWidth < imageWidth) {
             int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
 
             for (int imageX = image.getWidth() - totalWidth;
@@ -3550,6 +3616,22 @@ public class ECMA48Terminal extends LogicalScreen
             }
         }
 
+        if ((image.getWidth() != cells.size() * getTextWidth())
+            || (image.getHeight() != getTextHeight())
+        ) {
+            // Rescale the image to fit the text cells it is going into.
+            BufferedImage newImage;
+            newImage = new BufferedImage(cells.size() * getTextWidth(),
+                getTextHeight(), BufferedImage.TYPE_INT_ARGB);
+
+            java.awt.Graphics gr = newImage.getGraphics();
+            gr.drawImage(image, 0, 0, newImage.getWidth(),
+                newImage.getHeight(), null, null);
+            gr.dispose();
+            image = newImage;
+            fullHeight = image.getHeight();
+        }
+
         /*
          * From https://iterm2.com/documentation-images.html:
          *
@@ -3611,8 +3693,6 @@ public class ECMA48Terminal extends LogicalScreen
             return "";
         }
 
-        // iTerm2 does not advance the cursor automatically, so place it
-        // myself.
         sb.append("\033]1337;File=");
         /*
         sb.append(String.format("width=$d;height=1;preserveAspectRatio=1;",
@@ -3624,7 +3704,7 @@ public class ECMA48Terminal extends LogicalScreen
                     getTextHeight())));
          */
         sb.append("inline=1:");
-        sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
+        sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
         sb.append("\007");
 
         if (saveInCache) {
@@ -3681,7 +3761,6 @@ public class ECMA48Terminal extends LogicalScreen
 
         if (jexerCache == null) {
             jexerCache = new ImageCache(height * 10);
-            base64 = java.util.Base64.getEncoder();
         }
 
         // Save and get rows to/from the cache that do NOT have inverted
@@ -3709,8 +3788,8 @@ public class ECMA48Terminal extends LogicalScreen
         // Piece cells.get(x).getImage() pieces together into one larger
         // image for final rendering.
         int totalWidth = 0;
-        int fullWidth = cells.size() * getTextWidth();
-        int fullHeight = getTextHeight();
+        int fullWidth = cells.size() * imageWidth;
+        int fullHeight = imageHeight;
         for (int i = 0; i < cells.size(); i++) {
             totalWidth += cells.get(i).getImage().getWidth();
         }
@@ -3720,10 +3799,8 @@ public class ECMA48Terminal extends LogicalScreen
 
         int [] rgbArray;
         for (int i = 0; i < cells.size() - 1; i++) {
-            int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
-                imageWidth);
-            int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
-                imageHeight);
+            int tileWidth = imageWidth;
+            int tileHeight = imageHeight;
             if (false && cells.get(i).isInvertedImage()) {
                 // I used to put an all-white cell over the cursor, don't do
                 // that anymore.
@@ -3790,7 +3867,7 @@ public class ECMA48Terminal extends LogicalScreen
         image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
             imageHeight, rgbArray, 0, totalWidth);
 
-        if (totalWidth < getTextWidth()) {
+        if (totalWidth < imageWidth) {
             int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
 
             for (int imageX = image.getWidth() - totalWidth;
@@ -3802,6 +3879,22 @@ public class ECMA48Terminal extends LogicalScreen
             }
         }
 
+        if ((image.getWidth() != cells.size() * getTextWidth())
+            || (image.getHeight() != getTextHeight())
+        ) {
+            // Rescale the image to fit the text cells it is going into.
+            BufferedImage newImage;
+            newImage = new BufferedImage(cells.size() * getTextWidth(),
+                getTextHeight(), BufferedImage.TYPE_INT_ARGB);
+
+            java.awt.Graphics gr = newImage.getGraphics();
+            gr.drawImage(image, 0, 0, newImage.getWidth(),
+                newImage.getHeight(), null, null);
+            gr.dispose();
+            image = newImage;
+            fullHeight = image.getHeight();
+        }
+
         if (jexerImageOption == JexerImageOption.PNG) {
             // Encode as PNG
             ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024);
@@ -3819,7 +3912,7 @@ public class ECMA48Terminal extends LogicalScreen
             }
 
             sb.append("\033]444;1;0;");
-            sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
+            sb.append(StringUtils.toBase64(pngOutputStream.toByteArray()));
             sb.append("\007");
 
         } else if (jexerImageOption == JexerImageOption.JPG) {
@@ -3851,7 +3944,7 @@ public class ECMA48Terminal extends LogicalScreen
             }
 
             sb.append("\033]444;2;0;");
-            sb.append(base64.encodeToString(jpgOutputStream.toByteArray()));
+            sb.append(StringUtils.toBase64(jpgOutputStream.toByteArray()));
             sb.append("\007");
 
         } else if (jexerImageOption == JexerImageOption.RGB) {
@@ -3870,7 +3963,7 @@ public class ECMA48Terminal extends LogicalScreen
                     bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb         & 0xFF);
                 }
             }
-            sb.append(base64.encodeToString(bytes));
+            sb.append(StringUtils.toBase64(bytes));
             sb.append("\007");
         }