also check for text cell size
[fanfix.git] / src / jexer / backend / ECMA48Terminal.java
index f0b3e3cc20d7d936368a528b75062a77db087114..9884835591adba9cfa57530cca3759ee8fb6404e 100644 (file)
@@ -44,16 +44,17 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
-import java.util.LinkedList;
 
 import jexer.TImage;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
 import jexer.bits.Color;
+import jexer.event.TCommandEvent;
 import jexer.event.TInputEvent;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMouseEvent;
 import jexer.event.TResizeEvent;
+import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
@@ -80,14 +81,6 @@ public class ECMA48Terminal extends LogicalScreen
         MOUSE_SGR,
     }
 
-    /**
-     * Number of colors in the sixel palette.  Xterm 335 defines the max as
-     * 1024.
-     */
-    private static final int MAX_COLOR_REGISTERS = 1024;
-    // Black-and-white is possible too.
-    // private static final int MAX_COLOR_REGISTERS = 2;
-
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -174,6 +167,11 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private TResizeEvent windowResize = null;
 
+    /**
+     * If true, emit wide-char (CJK/Emoji) characters as sixel images.
+     */
+    private boolean wideCharImages = true;
+
     /**
      * Window width in pixels.  Used for sixel support.
      */
@@ -199,6 +197,13 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private SixelCache sixelCache = null;
 
+    /**
+     * Number of colors in the sixel palette.  Xterm 335 defines the max as
+     * 1024.  Valid values are: 2 (black and white), 256, 512, 1024, and
+     * 2048.
+     */
+    private int sixelPaletteSize = 1024;
+
     /**
      * If true, then we changed System.in and need to change it back.
      */
@@ -231,9 +236,27 @@ public class ECMA48Terminal extends LogicalScreen
      */
     private Object listener;
 
+    // Colors to map DOS colors to AWT colors.
+    private static java.awt.Color MYBLACK;
+    private static java.awt.Color MYRED;
+    private static java.awt.Color MYGREEN;
+    private static java.awt.Color MYYELLOW;
+    private static java.awt.Color MYBLUE;
+    private static java.awt.Color MYMAGENTA;
+    private static java.awt.Color MYCYAN;
+    private static java.awt.Color MYWHITE;
+    private static java.awt.Color MYBOLD_BLACK;
+    private static java.awt.Color MYBOLD_RED;
+    private static java.awt.Color MYBOLD_GREEN;
+    private static java.awt.Color MYBOLD_YELLOW;
+    private static java.awt.Color MYBOLD_BLUE;
+    private static java.awt.Color MYBOLD_MAGENTA;
+    private static java.awt.Color MYBOLD_CYAN;
+    private static java.awt.Color MYBOLD_WHITE;
+
     /**
      * SixelPalette is used to manage the conversion of images between 24-bit
-     * RGB color and a palette of MAX_COLOR_REGISTERS colors.
+     * RGB color and a palette of sixelPaletteSize colors.
      */
     private class SixelPalette {
 
@@ -246,7 +269,7 @@ public class ECMA48Terminal extends LogicalScreen
          * Map of color palette index for sixel output, from the order it was
          * generated by makePalette() to rgbColors.
          */
-        private int [] rgbSortedIndex = new int[MAX_COLOR_REGISTERS];
+        private int [] rgbSortedIndex = new int[sixelPaletteSize];
 
         /**
          * The color palette, organized by hue, saturation, and luminance.
@@ -344,7 +367,7 @@ public class ECMA48Terminal extends LogicalScreen
             int green = (color >>>  8) & 0xFF;
             int blue  =  color         & 0xFF;
 
-            if (MAX_COLOR_REGISTERS == 2) {
+            if (sixelPaletteSize == 2) {
                 if (((red * red) + (green * green) + (blue * blue)) < 35568) {
                     // Black
                     return 0;
@@ -426,7 +449,7 @@ public class ECMA48Terminal extends LogicalScreen
                     ((255 - blue) * (255 - blue))) < diff) {
 
                 // White is a closer match.
-                idx = MAX_COLOR_REGISTERS - 1;
+                idx = sixelPaletteSize - 1;
             }
             assert (idx != -1);
             return idx;
@@ -449,7 +472,7 @@ public class ECMA48Terminal extends LogicalScreen
         }
 
         /**
-         * Dither an image to a MAX_COLOR_REGISTERS palette.  The dithered
+         * Dither an image to a sixelPaletteSize palette.  The dithered
          * image cells will contain indexes into the palette.
          *
          * @param image the image to dither
@@ -472,7 +495,7 @@ public class ECMA48Terminal extends LogicalScreen
                         imageY) & 0xFFFFFF;
                     int colorIdx = matchColor(oldPixel);
                     assert (colorIdx >= 0);
-                    assert (colorIdx < MAX_COLOR_REGISTERS);
+                    assert (colorIdx < sixelPaletteSize);
                     int newPixel = rgbColors.get(colorIdx);
                     ditheredImage.setRGB(imageX, imageY, colorIdx);
 
@@ -670,11 +693,11 @@ public class ECMA48Terminal extends LogicalScreen
         private void makePalette() {
             // Generate the sixel palette.  Because we have no idea at this
             // layer which image(s) will be shown, we have to use a common
-            // palette with MAX_COLOR_REGISTERS colors for everything, and
+            // palette with sixelPaletteSize colors for everything, and
             // map the BufferedImage colors to their nearest neighbor in RGB
             // space.
 
-            if (MAX_COLOR_REGISTERS == 2) {
+            if (sixelPaletteSize == 2) {
                 rgbColors.add(0);
                 rgbColors.add(0xFFFFFF);
                 rgbSortedIndex[0] = 0;
@@ -695,13 +718,13 @@ public class ECMA48Terminal extends LogicalScreen
             satBits = 2;
             lumBits = 1;
 
-            assert (MAX_COLOR_REGISTERS >= 256);
-            assert ((MAX_COLOR_REGISTERS == 256)
-                || (MAX_COLOR_REGISTERS == 512)
-                || (MAX_COLOR_REGISTERS == 1024)
-                || (MAX_COLOR_REGISTERS == 2048));
+            assert (sixelPaletteSize >= 256);
+            assert ((sixelPaletteSize == 256)
+                || (sixelPaletteSize == 512)
+                || (sixelPaletteSize == 1024)
+                || (sixelPaletteSize == 2048));
 
-            switch (MAX_COLOR_REGISTERS) {
+            switch (sixelPaletteSize) {
             case 512:
                 hueBits = 5;
                 satBits = 2;
@@ -787,7 +810,7 @@ public class ECMA48Terminal extends LogicalScreen
             }
             // System.err.printf("\n</body></html>\n");
 
-            assert (rgbColors.size() == MAX_COLOR_REGISTERS);
+            assert (rgbColors.size() == sixelPaletteSize);
 
             /*
              * We need to sort rgbColors, so that toSixel() can know where
@@ -799,19 +822,19 @@ public class ECMA48Terminal extends LogicalScreen
             Collections.sort(rgbColors);
             HashMap<Integer, Integer> rgbColorIndices = null;
             rgbColorIndices = new HashMap<Integer, Integer>();
-            for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+            for (int i = 0; i < sixelPaletteSize; i++) {
                 rgbColorIndices.put(rgbColors.get(i), i);
             }
-            for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+            for (int i = 0; i < sixelPaletteSize; i++) {
                 int rawColor = rawRgbList.get(i);
                 rgbSortedIndex[i] = rgbColorIndices.get(rawColor);
             }
             if (DEBUG) {
-                for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+                for (int i = 0; i < sixelPaletteSize; i++) {
                     assert (rawRgbList != null);
                     int idx = rgbSortedIndex[i];
                     int rgbColor = rgbColors.get(idx);
-                    if ((idx != 0) && (idx != MAX_COLOR_REGISTERS - 1)) {
+                    if ((idx != 0) && (idx != sixelPaletteSize - 1)) {
                         /*
                         System.err.printf("%d %06x --> %d %06x\n",
                             i, rawRgbList.get(i), idx, rgbColors.get(idx));
@@ -824,7 +847,7 @@ public class ECMA48Terminal extends LogicalScreen
             // Set the dimmest color as true black, and the brightest as true
             // white.
             rgbColors.set(0, 0);
-            rgbColors.set(MAX_COLOR_REGISTERS - 1, 0xFFFFFF);
+            rgbColors.set(sixelPaletteSize - 1, 0xFFFFFF);
 
             /*
             System.err.printf("<html><body>\n");
@@ -849,7 +872,7 @@ public class ECMA48Terminal extends LogicalScreen
         public String emitPalette(final StringBuilder sb,
             final boolean [] used) {
 
-            for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+            for (int i = 0; i < sixelPaletteSize; i++) {
                 if (((used != null) && (used[i] == true)) || (used == null)) {
                     int rgbColor = rgbColors.get(i);
                     sb.append(String.format("#%d;2;%d;%d;%d", i,
@@ -1001,14 +1024,16 @@ public class ECMA48Terminal extends LogicalScreen
     // ------------------------------------------------------------------------
 
     /**
-     * Constructor sets up state for getEvent().
+     * Constructor sets up state for getEvent().  If either windowWidth or
+     * windowHeight are less than 1, the terminal is not resized.
      *
      * @param listener the object this backend needs to wake up when new
      * input comes in
      * @param input an InputStream connected to the remote user, or null for
      * System.in.  If System.in is used, then on non-Windows systems it will
-     * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
-     * mode.  input is always converted to a Reader with UTF-8 encoding.
+     * be put in raw mode; closeTerminal() will (blindly!) put System.in in
+     * cooked mode.  input is always converted to a Reader with UTF-8
+     * encoding.
      * @param output an OutputStream connected to the remote user, or null
      * for System.out.  output is always converted to a Writer with UTF-8
      * encoding.
@@ -1025,10 +1050,12 @@ public class ECMA48Terminal extends LogicalScreen
 
         // Send dtterm/xterm sequences, which will probably not work because
         // allowWindowOps is defaulted to false.
-        String resizeString = String.format("\033[8;%d;%dt", windowHeight,
-            windowWidth);
-        this.output.write(resizeString);
-        this.output.flush();
+        if ((windowWidth > 0) && (windowHeight > 0)) {
+            String resizeString = String.format("\033[8;%d;%dt", windowHeight,
+                windowWidth);
+            this.output.write(resizeString);
+            this.output.flush();
+        }
     }
 
     /**
@@ -1038,8 +1065,9 @@ public class ECMA48Terminal extends LogicalScreen
      * input comes in
      * @param input an InputStream connected to the remote user, or null for
      * System.in.  If System.in is used, then on non-Windows systems it will
-     * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
-     * mode.  input is always converted to a Reader with UTF-8 encoding.
+     * be put in raw mode; closeTerminal() will (blindly!) put System.in in
+     * cooked mode.  input is always converted to a Reader with UTF-8
+     * encoding.
      * @param output an OutputStream connected to the remote user, or null
      * for System.out.  output is always converted to a Writer with UTF-8
      * encoding.
@@ -1107,7 +1135,7 @@ public class ECMA48Terminal extends LogicalScreen
         reloadOptions();
 
         // Spin up the input reader
-        eventQueue = new LinkedList<TInputEvent>();
+        eventQueue = new ArrayList<TInputEvent>();
         readerThread = new Thread(this);
         readerThread.start();
 
@@ -1193,7 +1221,7 @@ public class ECMA48Terminal extends LogicalScreen
         reloadOptions();
 
         // Spin up the input reader
-        eventQueue = new LinkedList<TInputEvent>();
+        eventQueue = new ArrayList<TInputEvent>();
         readerThread = new Thread(this);
         readerThread.start();
 
@@ -1306,7 +1334,7 @@ public class ECMA48Terminal extends LogicalScreen
      */
     public void closeTerminal() {
 
-        // System.err.println("=== shutdown() ==="); System.err.flush();
+        // System.err.println("=== closeTerminal() ==="); System.err.flush();
 
         // Tell the reader thread to stop looking at input
         stopReaderThread = true;
@@ -1321,7 +1349,7 @@ public class ECMA48Terminal extends LogicalScreen
         // Disable mouse reporting and show cursor.  Defensive null check
         // here in case closeTerminal() is called twice.
         if (output != null) {
-            output.printf("%s%s%s", mouse(false), cursor(true), normal());
+            output.printf("%s%s%s", mouse(false), cursor(true), defaultColor());
             output.flush();
         }
 
@@ -1370,12 +1398,44 @@ public class ECMA48Terminal extends LogicalScreen
             doRgbColor = false;
         }
 
+        // Default to using sixel for full-width characters.
+        if (System.getProperty("jexer.ECMA48.wideCharImages",
+                "true").equals("true")) {
+            wideCharImages = true;
+        } else {
+            wideCharImages = false;
+        }
+
         // Pull the system properties for sixel output.
         if (System.getProperty("jexer.ECMA48.sixel", "true").equals("true")) {
             sixel = true;
         } else {
             sixel = false;
         }
+
+        // Palette size
+        int paletteSize = 1024;
+        try {
+            paletteSize = Integer.parseInt(System.getProperty(
+                "jexer.ECMA48.sixelPaletteSize", "1024"));
+            switch (paletteSize) {
+            case 2:
+            case 256:
+            case 512:
+            case 1024:
+            case 2048:
+                sixelPaletteSize = paletteSize;
+                break;
+            default:
+                // Ignore value
+                break;
+            }
+        } catch (NumberFormatException e) {
+            // SQUASH
+        }
+
+        // Set custom colors
+        setCustomSystemColors();
     }
 
     // ------------------------------------------------------------------------
@@ -1390,7 +1450,7 @@ public class ECMA48Terminal extends LogicalScreen
         // available() will often return > 1, so we need to read in chunks to
         // stay caught up.
         char [] readBuffer = new char[128];
-        List<TInputEvent> events = new LinkedList<TInputEvent>();
+        List<TInputEvent> events = new ArrayList<TInputEvent>();
 
         while (!done && !stopReaderThread) {
             try {
@@ -1455,6 +1515,11 @@ public class ECMA48Terminal extends LogicalScreen
                         events.clear();
                     }
 
+                    if (output.checkError()) {
+                        // This is EOF.
+                        done = true;
+                    }
+
                     // Wait 20 millis for more data
                     Thread.sleep(20);
                 }
@@ -1466,6 +1531,17 @@ public class ECMA48Terminal extends LogicalScreen
                 done = true;
             }
         } // while ((done == false) && (stopReaderThread == false))
+
+        // Pass an event up to TApplication to tell it this Backend is done.
+        synchronized (eventQueue) {
+            eventQueue.add(new TCommandEvent(cmBackendDisconnect));
+        }
+        if (listener != null) {
+            synchronized (listener) {
+                listener.notifyAll();
+            }
+        }
+
         // System.err.println("*** run() exiting..."); System.err.flush();
     }
 
@@ -1654,7 +1730,11 @@ public class ECMA48Terminal extends LogicalScreen
 
                 // Image cell: bypass the rest of the loop, it is not
                 // rendered here.
-                if (lCell.isImage()) {
+                if ((wideCharImages && lCell.isImage())
+                    || (!wideCharImages
+                        && lCell.isImage()
+                        && (lCell.getWidth() == Cell.Width.SINGLE))
+                ) {
                     hasImage = true;
 
                     // Save the last rendered cell
@@ -1665,7 +1745,16 @@ public class ECMA48Terminal extends LogicalScreen
                     continue;
                 }
 
-                assert (!lCell.isImage());
+                assert ((wideCharImages && !lCell.isImage())
+                    || (!wideCharImages
+                        && (!lCell.isImage()
+                            || (lCell.isImage()
+                                && (lCell.getWidth() != Cell.Width.SINGLE)))));
+
+                if (!wideCharImages && (lCell.getWidth() == Cell.Width.RIGHT)) {
+                    continue;
+                }
+
                 if (hasImage) {
                     hasImage = false;
                     sb.append(gotoXY(x, y));
@@ -1825,7 +1914,13 @@ public class ECMA48Terminal extends LogicalScreen
 
                 }
                 // Emit the character
-                sb.append(lCell.getChar());
+                if (wideCharImages
+                    // Don't emit the right-half of full-width chars.
+                    || (!wideCharImages
+                        && (lCell.getWidth() != Cell.Width.RIGHT))
+                ) {
+                    sb.append(Character.toChars(lCell.getChar()));
+                }
 
                 // Save the last rendered cell
                 lastX = x;
@@ -1877,7 +1972,10 @@ public class ECMA48Terminal extends LogicalScreen
                 Cell lCell = logical[x][y];
                 Cell pCell = physical[x][y];
 
-                if (!lCell.isImage()) {
+                if (!lCell.isImage()
+                    || (!wideCharImages
+                        && (lCell.getWidth() != Cell.Width.SINGLE))
+                ) {
                     continue;
                 }
 
@@ -2662,6 +2760,27 @@ public class ECMA48Terminal extends LogicalScreen
                             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:
@@ -2696,7 +2815,9 @@ public class ECMA48Terminal extends LogicalScreen
      * @return the string to emit to xterm
      */
     private String xtermReportWindowPixelDimensions() {
-        return "\033[14t";
+        // We will ask for both window and text cell dimensions, and
+        // hopefully one of them will work.
+        return "\033[14t\033[16t";
     }
 
     /**
@@ -2728,6 +2849,46 @@ public class ECMA48Terminal extends LogicalScreen
     // Sixel output support ---------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Get the number of colors in the sixel palette.
+     *
+     * @return the palette size
+     */
+    public int getSixelPaletteSize() {
+        return sixelPaletteSize;
+    }
+
+    /**
+     * Set the number of colors in the sixel palette.
+     *
+     * @param paletteSize the new palette size
+     */
+    public void setSixelPaletteSize(final int paletteSize) {
+        if (paletteSize == sixelPaletteSize) {
+            return;
+        }
+
+        switch (paletteSize) {
+        case 2:
+        case 256:
+        case 512:
+        case 1024:
+        case 2048:
+            break;
+        default:
+            throw new IllegalArgumentException("Unsupported sixel palette " +
+                " size: " + paletteSize);
+        }
+
+        // Don't step on the screen refresh thread.
+        synchronized (this) {
+            sixelPaletteSize = paletteSize;
+            palette = null;
+            sixelCache = null;
+            clearPhysical();
+        }
+    }
+
     /**
      * Start a sixel string for display one row's worth of bitmap data.
      *
@@ -2834,14 +2995,32 @@ public class ECMA48Terminal extends LogicalScreen
 
         int [] rgbArray;
         for (int i = 0; i < cells.size() - 1; i++) {
-            if (cells.get(i).isInvertedImage()) {
+            int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
+                imageWidth);
+            int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
+                imageHeight);
+            if (false && cells.get(i).isInvertedImage()) {
+                // I used to put an all-white cell over the cursor, don't do
+                // that anymore.
                 rgbArray = new int[imageWidth * imageHeight];
                 for (int j = 0; j < rgbArray.length; j++) {
                     rgbArray[j] = 0xFFFFFF;
                 }
             } else {
-                rgbArray = cells.get(i).getImage().getRGB(0, 0,
-                    imageWidth, imageHeight, null, 0, imageWidth);
+                try {
+                    rgbArray = cells.get(i).getImage().getRGB(0, 0,
+                        tileWidth, tileHeight, null, 0, tileWidth);
+                } catch (Exception e) {
+                    throw new RuntimeException("image " + imageWidth + "x" +
+                        imageHeight +
+                        "tile " + tileWidth + "x" +
+                        tileHeight +
+                        " cells.get(i).getImage() " +
+                        cells.get(i).getImage() +
+                        " i " + i +
+                        " fullWidth " + fullWidth +
+                        " fullHeight " + fullHeight, e);
+                }
             }
 
             /*
@@ -2852,9 +3031,9 @@ public class ECMA48Terminal extends LogicalScreen
                 fullWidth, fullHeight, cells.size(), getTextWidth());
              */
 
-            image.setRGB(i * imageWidth, 0, imageWidth, imageHeight,
-                rgbArray, 0, imageWidth);
-            if (imageHeight < fullHeight) {
+            image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
+                rgbArray, 0, tileWidth);
+            if (tileHeight < fullHeight) {
                 int backgroundColor = cells.get(i).getBackground().getRGB();
                 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
                     for (int imageY = imageHeight; imageY < fullHeight;
@@ -2866,14 +3045,22 @@ public class ECMA48Terminal extends LogicalScreen
             }
         }
         totalWidth -= ((cells.size() - 1) * imageWidth);
-        if (cells.get(cells.size() - 1).isInvertedImage()) {
+        if (false && cells.get(cells.size() - 1).isInvertedImage()) {
+            // I used to put an all-white cell over the cursor, don't do that
+            // anymore.
             rgbArray = new int[totalWidth * imageHeight];
             for (int j = 0; j < rgbArray.length; j++) {
                 rgbArray[j] = 0xFFFFFF;
             }
         } else {
-            rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
-                totalWidth, imageHeight, null, 0, totalWidth);
+            try {
+                rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
+                    totalWidth, imageHeight, null, 0, totalWidth);
+            } catch (Exception e) {
+                throw new RuntimeException("image " + imageWidth + "x" +
+                    imageHeight + " cells.get(cells.size() - 1).getImage() " +
+                    cells.get(cells.size() - 1).getImage(), e);
+            }
         }
         image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
             imageHeight, rgbArray, 0, totalWidth);
@@ -2898,7 +3085,7 @@ public class ECMA48Terminal extends LogicalScreen
 
         // Emit the palette, but only for the colors actually used by these
         // cells.
-        boolean [] usedColors = new boolean[MAX_COLOR_REGISTERS];
+        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;
@@ -2918,13 +3105,13 @@ public class ECMA48Terminal extends LogicalScreen
 
                     int colorIdx = image.getRGB(imageX, imageY + currentRow);
                     assert (colorIdx >= 0);
-                    assert (colorIdx < MAX_COLOR_REGISTERS);
+                    assert (colorIdx < sixelPaletteSize);
 
                     sixels[imageX][imageY] = colorIdx;
                 }
             }
 
-            for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+            for (int i = 0; i < sixelPaletteSize; i++) {
                 boolean isUsed = false;
                 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
                     for (int j = 0; j < 6; j++) {
@@ -2941,6 +3128,8 @@ public class ECMA48Terminal extends LogicalScreen
                 // colored pixels, and select the color.
                 sb.append(String.format("$#%d", i));
 
+                int oldData = -1;
+                int oldDataCount = 0;
                 for (int imageX = 0; imageX < image.getWidth(); imageX++) {
 
                     // Add up all the pixels that match this color.
@@ -2973,11 +3162,33 @@ public class ECMA48Terminal extends LogicalScreen
                         }
                     }
                     assert (data >= 0);
-                    assert (data < 127);
+                    assert (data < 64);
                     data += 63;
-                    sb.append((char) data);
+
+                    if (data == oldData) {
+                        oldDataCount++;
+                    } else {
+                        if (oldDataCount == 1) {
+                            sb.append((char) oldData);
+                        } else if (oldDataCount > 1) {
+                            sb.append(String.format("!%d", oldDataCount));
+                            sb.append((char) oldData);
+                        }
+                        oldDataCount = 1;
+                        oldData = data;
+                    }
+
                 } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
-            } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++)
+
+                // Emit the last sequence.
+                if (oldDataCount == 1) {
+                    sb.append((char) oldData);
+                } else if (oldDataCount > 1) {
+                    sb.append(String.format("!%d", oldDataCount));
+                    sb.append((char) oldData);
+                }
+
+            } // for (int i = 0; i < sixelPaletteSize; i++)
 
             // Advance to the next scan line.
             sb.append("-");
@@ -2995,10 +3206,109 @@ public class ECMA48Terminal extends LogicalScreen
         return (startSixel(x, y) + sb.toString() + endSixel());
     }
 
+    /**
+     * Get the sixel support flag.
+     *
+     * @return true if this terminal is emitting sixel
+     */
+    public boolean hasSixel() {
+        return sixel;
+    }
+
     // ------------------------------------------------------------------------
     // End sixel output support -----------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Setup system colors to match DOS color palette.
+     */
+    private void setDOSColors() {
+        MYBLACK         = new java.awt.Color(0x00, 0x00, 0x00);
+        MYRED           = new java.awt.Color(0xa8, 0x00, 0x00);
+        MYGREEN         = new java.awt.Color(0x00, 0xa8, 0x00);
+        MYYELLOW        = new java.awt.Color(0xa8, 0x54, 0x00);
+        MYBLUE          = new java.awt.Color(0x00, 0x00, 0xa8);
+        MYMAGENTA       = new java.awt.Color(0xa8, 0x00, 0xa8);
+        MYCYAN          = new java.awt.Color(0x00, 0xa8, 0xa8);
+        MYWHITE         = new java.awt.Color(0xa8, 0xa8, 0xa8);
+        MYBOLD_BLACK    = new java.awt.Color(0x54, 0x54, 0x54);
+        MYBOLD_RED      = new java.awt.Color(0xfc, 0x54, 0x54);
+        MYBOLD_GREEN    = new java.awt.Color(0x54, 0xfc, 0x54);
+        MYBOLD_YELLOW   = new java.awt.Color(0xfc, 0xfc, 0x54);
+        MYBOLD_BLUE     = new java.awt.Color(0x54, 0x54, 0xfc);
+        MYBOLD_MAGENTA  = new java.awt.Color(0xfc, 0x54, 0xfc);
+        MYBOLD_CYAN     = new java.awt.Color(0x54, 0xfc, 0xfc);
+        MYBOLD_WHITE    = new java.awt.Color(0xfc, 0xfc, 0xfc);
+    }
+
+    /**
+     * Setup ECMA48 colors to match those provided in system properties.
+     */
+    private void setCustomSystemColors() {
+        setDOSColors();
+
+        MYBLACK   = getCustomColor("jexer.ECMA48.color0", MYBLACK);
+        MYRED     = getCustomColor("jexer.ECMA48.color1", MYRED);
+        MYGREEN   = getCustomColor("jexer.ECMA48.color2", MYGREEN);
+        MYYELLOW  = getCustomColor("jexer.ECMA48.color3", MYYELLOW);
+        MYBLUE    = getCustomColor("jexer.ECMA48.color4", MYBLUE);
+        MYMAGENTA = getCustomColor("jexer.ECMA48.color5", MYMAGENTA);
+        MYCYAN    = getCustomColor("jexer.ECMA48.color6", MYCYAN);
+        MYWHITE   = getCustomColor("jexer.ECMA48.color7", MYWHITE);
+        MYBOLD_BLACK   = getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK);
+        MYBOLD_RED     = getCustomColor("jexer.ECMA48.color9", MYBOLD_RED);
+        MYBOLD_GREEN   = getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN);
+        MYBOLD_YELLOW  = getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW);
+        MYBOLD_BLUE    = getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE);
+        MYBOLD_MAGENTA = getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA);
+        MYBOLD_CYAN    = getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN);
+        MYBOLD_WHITE   = getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE);
+    }
+
+    /**
+     * Setup one system color to match the RGB value provided in system
+     * properties.
+     *
+     * @param key the system property key
+     * @param defaultColor the default color to return if key is not set, or
+     * incorrect
+     * @return a color from the RGB string, or defaultColor
+     */
+    private java.awt.Color getCustomColor(final String key,
+        final java.awt.Color defaultColor) {
+
+        String rgb = System.getProperty(key);
+        if (rgb == null) {
+            return defaultColor;
+        }
+        if (rgb.startsWith("#")) {
+            rgb = rgb.substring(1);
+        }
+        int rgbInt = 0;
+        try {
+            rgbInt = Integer.parseInt(rgb, 16);
+        } catch (NumberFormatException e) {
+            return defaultColor;
+        }
+        java.awt.Color color = new java.awt.Color((rgbInt & 0xFF0000) >>> 16,
+            (rgbInt & 0x00FF00) >>> 8,
+            (rgbInt & 0x0000FF));
+
+        return color;
+    }
+
+    /**
+     * Create a T.416 RGB parameter sequence for a custom system color.
+     *
+     * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors
+     * @return the color portion of the string to emit to an ANSI /
+     * ECMA-style terminal
+     */
+    private String systemColorRGB(final java.awt.Color color) {
+        return String.format("%d;%d;%d", color.getRed(), color.getGreen(),
+            color.getBlue());
+    }
+
     /**
      * Create a SGR parameter sequence for a single color change.
      *
@@ -3082,21 +3392,21 @@ public class ECMA48Terminal extends LogicalScreen
             // Bold implies foreground only
             sb.append("38;2;");
             if (color.equals(Color.BLACK)) {
-                sb.append("84;84;84");
+                sb.append(systemColorRGB(MYBOLD_BLACK));
             } else if (color.equals(Color.RED)) {
-                sb.append("252;84;84");
+                sb.append(systemColorRGB(MYBOLD_RED));
             } else if (color.equals(Color.GREEN)) {
-                sb.append("84;252;84");
+                sb.append(systemColorRGB(MYBOLD_GREEN));
             } else if (color.equals(Color.YELLOW)) {
-                sb.append("252;252;84");
+                sb.append(systemColorRGB(MYBOLD_YELLOW));
             } else if (color.equals(Color.BLUE)) {
-                sb.append("84;84;252");
+                sb.append(systemColorRGB(MYBOLD_BLUE));
             } else if (color.equals(Color.MAGENTA)) {
-                sb.append("252;84;252");
+                sb.append(systemColorRGB(MYBOLD_MAGENTA));
             } else if (color.equals(Color.CYAN)) {
-                sb.append("84;252;252");
+                sb.append(systemColorRGB(MYBOLD_CYAN));
             } else if (color.equals(Color.WHITE)) {
-                sb.append("252;252;252");
+                sb.append(systemColorRGB(MYBOLD_WHITE));
             }
         } else {
             if (foreground) {
@@ -3105,21 +3415,21 @@ public class ECMA48Terminal extends LogicalScreen
                 sb.append("48;2;");
             }
             if (color.equals(Color.BLACK)) {
-                sb.append("0;0;0");
+                sb.append(systemColorRGB(MYBLACK));
             } else if (color.equals(Color.RED)) {
-                sb.append("168;0;0");
+                sb.append(systemColorRGB(MYRED));
             } else if (color.equals(Color.GREEN)) {
-                sb.append("0;168;0");
+                sb.append(systemColorRGB(MYGREEN));
             } else if (color.equals(Color.YELLOW)) {
-                sb.append("168;84;0");
+                sb.append(systemColorRGB(MYYELLOW));
             } else if (color.equals(Color.BLUE)) {
-                sb.append("0;0;168");
+                sb.append(systemColorRGB(MYBLUE));
             } else if (color.equals(Color.MAGENTA)) {
-                sb.append("168;0;168");
+                sb.append(systemColorRGB(MYMAGENTA));
             } else if (color.equals(Color.CYAN)) {
-                sb.append("0;168;168");
+                sb.append(systemColorRGB(MYCYAN));
             } else if (color.equals(Color.WHITE)) {
-                sb.append("168;168;168");
+                sb.append(systemColorRGB(MYWHITE));
             }
         }
         sb.append("m");
@@ -3356,7 +3666,7 @@ public class ECMA48Terminal extends LogicalScreen
     }
 
     /**
-     * Create a SGR parameter sequence to reset to defaults.
+     * Create a SGR parameter sequence to reset to VT100 defaults.
      *
      * @return the string to emit to an ANSI / ECMA-style terminal,
      * e.g. "\033[0m"
@@ -3365,6 +3675,29 @@ public class ECMA48Terminal extends LogicalScreen
         return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK);
     }
 
+    /**
+     * Create a SGR parameter sequence to reset to ECMA-48 default
+     * foreground/background.
+     *
+     * @return the string to emit to an ANSI / ECMA-style terminal,
+     * e.g. "\033[0m"
+     */
+    private String defaultColor() {
+        /*
+         * VT100 normal.
+         * Normal (neither bold nor faint).
+         * Not italicized.
+         * Not underlined.
+         * Steady (not blinking).
+         * Positive (not inverse).
+         * Visible (not hidden).
+         * Not crossed-out.
+         * Default foreground color.
+         * Default background color.
+         */
+        return "\033[0;22;23;24;25;27;28;29;39;49m";
+    }
+
     /**
      * Create a SGR parameter sequence to reset to defaults.
      *