*
* The MIT License (MIT)
*
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
*/
package jexer.backend;
+import java.awt.image.BufferedImage;
import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
-import java.util.LinkedList;
+import javax.imageio.ImageIO;
+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.*;
/**
*/
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.
+ */
+ private int widthPixels = 640;
+
+ /**
+ * Window height in pixels. Used for sixel support.
+ */
+ private int heightPixels = 400;
+
+ /**
+ * If true, emit image data via sixel.
+ */
+ private boolean sixel = true;
+
+ /**
+ * The sixel palette handler.
+ */
+ private SixelPalette palette = null;
+
+ /**
+ * The sixel post-rendered string cache.
+ */
+ private ImageCache 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, emit image data via iTerm2 image protocol.
+ */
+ private boolean iterm2Images = false;
+
+ /**
+ * The iTerm2 post-rendered string cache.
+ */
+ private ImageCache iterm2Cache = null;
+
+ /**
+ * If true, emit image data via Jexer image protocol.
+ */
+ private boolean jexerImages = false;
+
+ /**
+ * The Jexer post-rendered string cache.
+ */
+ 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.
*/
- private boolean setRawMode;
+ private boolean setRawMode = false;
+
+ /**
+ * If true, '?' was seen in terminal response.
+ */
+ private boolean decPrivateModeFlag = false;
/**
* The terminal's input. If an InputStream is not specified in the
*/
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 sixelPaletteSize colors.
+ */
+ private class SixelPalette {
+
+ /**
+ * Color palette for sixel output, sorted low to high.
+ */
+ private List<Integer> rgbColors = new ArrayList<Integer>();
+
+ /**
+ * Map of color palette index for sixel output, from the order it was
+ * generated by makePalette() to rgbColors.
+ */
+ private int [] rgbSortedIndex = new int[sixelPaletteSize];
+
+ /**
+ * The color palette, organized by hue, saturation, and luminance.
+ * This is used for a fast color match.
+ */
+ private ArrayList<ArrayList<ArrayList<ColorIdx>>> hslColors;
+
+ /**
+ * Number of bits for hue.
+ */
+ private int hueBits = -1;
+
+ /**
+ * Number of bits for saturation.
+ */
+ private int satBits = -1;
+
+ /**
+ * Number of bits for luminance.
+ */
+ private int lumBits = -1;
+
+ /**
+ * Step size for hue bins.
+ */
+ private int hueStep = -1;
+
+ /**
+ * Step size for saturation bins.
+ */
+ private int satStep = -1;
+
+ /**
+ * Cached RGB to HSL result.
+ */
+ private int hsl[] = new int[3];
+
+ /**
+ * ColorIdx records a RGB color and its palette index.
+ */
+ private class ColorIdx {
+ /**
+ * The 24-bit RGB color.
+ */
+ public int color;
+
+ /**
+ * The palette index for this color.
+ */
+ public int index;
+
+ /**
+ * Public constructor.
+ *
+ * @param color the 24-bit RGB color
+ * @param index the palette index for this color
+ */
+ public ColorIdx(final int color, final int index) {
+ this.color = color;
+ this.index = index;
+ }
+ }
+
+ /**
+ * Public constructor.
+ */
+ public SixelPalette() {
+ makePalette();
+ }
+
+ /**
+ * Find the nearest match for a color in the palette.
+ *
+ * @param color the RGB color
+ * @return the index in rgbColors that is closest to color
+ */
+ public int matchColor(final int color) {
+
+ assert (color >= 0);
+
+ /*
+ * matchColor() is a critical performance bottleneck. To make it
+ * decent, we do the following:
+ *
+ * 1. Find the nearest two hues that bracket this color.
+ *
+ * 2. Find the nearest two saturations that bracket this color.
+ *
+ * 3. Iterate within these four bands of luminance values,
+ * returning the closest color by Euclidean distance.
+ *
+ * This strategy reduces the search space by about 97%.
+ */
+ int red = (color >>> 16) & 0xFF;
+ int green = (color >>> 8) & 0xFF;
+ int blue = color & 0xFF;
+
+ if (sixelPaletteSize == 2) {
+ if (((red * red) + (green * green) + (blue * blue)) < 35568) {
+ // Black
+ return 0;
+ }
+ // White
+ return 1;
+ }
+
+
+ rgbToHsl(red, green, blue, hsl);
+ int hue = hsl[0];
+ int sat = hsl[1];
+ int lum = hsl[2];
+ // System.err.printf("%d %d %d\n", hue, sat, lum);
+
+ double diff = Double.MAX_VALUE;
+ int idx = -1;
+
+ int hue1 = hue / (360/hueStep);
+ int hue2 = hue1 + 1;
+ if (hue1 >= hslColors.size() - 1) {
+ // Bracket pure red from above.
+ hue1 = hslColors.size() - 1;
+ hue2 = 0;
+ } else if (hue1 == 0) {
+ // Bracket pure red from below.
+ hue2 = hslColors.size() - 1;
+ }
+
+ for (int hI = hue1; hI != -1;) {
+ ArrayList<ArrayList<ColorIdx>> sats = hslColors.get(hI);
+ if (hI == hue1) {
+ hI = hue2;
+ } else if (hI == hue2) {
+ hI = -1;
+ }
+
+ int sMin = (sat / satStep) - 1;
+ int sMax = sMin + 1;
+ if (sMin < 0) {
+ sMin = 0;
+ sMax = 1;
+ } else if (sMin == sats.size() - 1) {
+ sMax = sMin;
+ sMin--;
+ }
+ assert (sMin >= 0);
+ assert (sMax - sMin == 1);
+
+ // int sMin = 0;
+ // int sMax = sats.size() - 1;
+
+ for (int sI = sMin; sI <= sMax; sI++) {
+ ArrayList<ColorIdx> lums = sats.get(sI);
+
+ // True 3D colorspace match for the remaining values
+ for (ColorIdx c: lums) {
+ int rgbColor = c.color;
+ double newDiff = 0;
+ int red2 = (rgbColor >>> 16) & 0xFF;
+ int green2 = (rgbColor >>> 8) & 0xFF;
+ int blue2 = rgbColor & 0xFF;
+ newDiff += Math.pow(red2 - red, 2);
+ newDiff += Math.pow(green2 - green, 2);
+ newDiff += Math.pow(blue2 - blue, 2);
+ if (newDiff < diff) {
+ idx = rgbSortedIndex[c.index];
+ diff = newDiff;
+ }
+ }
+ }
+ }
+
+ if (((red * red) + (green * green) + (blue * blue)) < diff) {
+ // Black is a closer match.
+ idx = 0;
+ } else if ((((255 - red) * (255 - red)) +
+ ((255 - green) * (255 - green)) +
+ ((255 - blue) * (255 - blue))) < diff) {
+
+ // White is a closer match.
+ idx = sixelPaletteSize - 1;
+ }
+ assert (idx != -1);
+ return idx;
+ }
+
+ /**
+ * Clamp an int value to [0, 255].
+ *
+ * @param x the int value
+ * @return an int between 0 and 255.
+ */
+ private int clamp(final int x) {
+ if (x < 0) {
+ return 0;
+ }
+ if (x > 255) {
+ return 255;
+ }
+ return x;
+ }
+
+ /**
+ * Dither an image to a sixelPaletteSize palette. The dithered
+ * image cells will contain indexes into the palette.
+ *
+ * @param image the image to dither
+ * @return the dithered image. Every pixel is an index into the
+ * palette.
+ */
+ public BufferedImage ditherImage(final BufferedImage image) {
+
+ BufferedImage ditheredImage = new BufferedImage(image.getWidth(),
+ image.getHeight(), BufferedImage.TYPE_INT_ARGB);
+
+ int [] rgbArray = image.getRGB(0, 0, image.getWidth(),
+ image.getHeight(), null, 0, image.getWidth());
+ ditheredImage.setRGB(0, 0, image.getWidth(), image.getHeight(),
+ rgbArray, 0, image.getWidth());
+
+ for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ int oldPixel = ditheredImage.getRGB(imageX,
+ imageY) & 0xFFFFFF;
+ int colorIdx = matchColor(oldPixel);
+ assert (colorIdx >= 0);
+ assert (colorIdx < sixelPaletteSize);
+ int newPixel = rgbColors.get(colorIdx);
+ ditheredImage.setRGB(imageX, imageY, colorIdx);
+
+ int oldRed = (oldPixel >>> 16) & 0xFF;
+ int oldGreen = (oldPixel >>> 8) & 0xFF;
+ int oldBlue = oldPixel & 0xFF;
+
+ int newRed = (newPixel >>> 16) & 0xFF;
+ int newGreen = (newPixel >>> 8) & 0xFF;
+ int newBlue = newPixel & 0xFF;
+
+ int redError = (oldRed - newRed) / 16;
+ int greenError = (oldGreen - newGreen) / 16;
+ int blueError = (oldBlue - newBlue) / 16;
+
+ int red, green, blue;
+ if (imageX < image.getWidth() - 1) {
+ int pXpY = ditheredImage.getRGB(imageX + 1, imageY);
+ red = ((pXpY >>> 16) & 0xFF) + (7 * redError);
+ green = ((pXpY >>> 8) & 0xFF) + (7 * greenError);
+ blue = ( pXpY & 0xFF) + (7 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXpY = ((red & 0xFF) << 16);
+ pXpY |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX + 1, imageY, pXpY);
+
+ if (imageY < image.getHeight() - 1) {
+ int pXpYp = ditheredImage.getRGB(imageX + 1,
+ imageY + 1);
+ red = ((pXpYp >>> 16) & 0xFF) + redError;
+ green = ((pXpYp >>> 8) & 0xFF) + greenError;
+ blue = ( pXpYp & 0xFF) + blueError;
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXpYp = ((red & 0xFF) << 16);
+ pXpYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX + 1, imageY + 1, pXpYp);
+ }
+ } else if (imageY < image.getHeight() - 1) {
+ int pXmYp = ditheredImage.getRGB(imageX - 1,
+ imageY + 1);
+ int pXYp = ditheredImage.getRGB(imageX,
+ imageY + 1);
+
+ red = ((pXmYp >>> 16) & 0xFF) + (3 * redError);
+ green = ((pXmYp >>> 8) & 0xFF) + (3 * greenError);
+ blue = ( pXmYp & 0xFF) + (3 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXmYp = ((red & 0xFF) << 16);
+ pXmYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX - 1, imageY + 1, pXmYp);
+
+ red = ((pXYp >>> 16) & 0xFF) + (5 * redError);
+ green = ((pXYp >>> 8) & 0xFF) + (5 * greenError);
+ blue = ( pXYp & 0xFF) + (5 * blueError);
+ red = clamp(red);
+ green = clamp(green);
+ blue = clamp(blue);
+ pXYp = ((red & 0xFF) << 16);
+ pXYp |= ((green & 0xFF) << 8) | (blue & 0xFF);
+ ditheredImage.setRGB(imageX, imageY + 1, pXYp);
+ }
+ } // for (int imageY = 0; imageY < image.getHeight(); imageY++)
+ } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
+
+ return ditheredImage;
+ }
+
+ /**
+ * Convert an RGB color to HSL.
+ *
+ * @param red red color, between 0 and 255
+ * @param green green color, between 0 and 255
+ * @param blue blue color, between 0 and 255
+ * @param hsl the hsl color as [hue, saturation, luminance]
+ */
+ private void rgbToHsl(final int red, final int green,
+ final int blue, final int [] hsl) {
+
+ assert ((red >= 0) && (red <= 255));
+ assert ((green >= 0) && (green <= 255));
+ assert ((blue >= 0) && (blue <= 255));
+
+ double R = red / 255.0;
+ double G = green / 255.0;
+ double B = blue / 255.0;
+ boolean Rmax = false;
+ boolean Gmax = false;
+ boolean Bmax = false;
+ double min = (R < G ? R : G);
+ min = (min < B ? min : B);
+ double max = 0;
+ if ((R >= G) && (R >= B)) {
+ max = R;
+ Rmax = true;
+ } else if ((G >= R) && (G >= B)) {
+ max = G;
+ Gmax = true;
+ } else if ((B >= G) && (B >= R)) {
+ max = B;
+ Bmax = true;
+ }
+
+ double L = (min + max) / 2.0;
+ double H = 0.0;
+ double S = 0.0;
+ if (min != max) {
+ if (L < 0.5) {
+ S = (max - min) / (max + min);
+ } else {
+ S = (max - min) / (2.0 - max - min);
+ }
+ }
+ if (Rmax) {
+ assert (Gmax == false);
+ assert (Bmax == false);
+ H = (G - B) / (max - min);
+ } else if (Gmax) {
+ assert (Rmax == false);
+ assert (Bmax == false);
+ H = 2.0 + (B - R) / (max - min);
+ } else if (Bmax) {
+ assert (Rmax == false);
+ assert (Gmax == false);
+ H = 4.0 + (R - G) / (max - min);
+ }
+ if (H < 0.0) {
+ H += 6.0;
+ }
+ hsl[0] = (int) (H * 60.0);
+ hsl[1] = (int) (S * 100.0);
+ hsl[2] = (int) (L * 100.0);
+
+ assert ((hsl[0] >= 0) && (hsl[0] <= 360));
+ assert ((hsl[1] >= 0) && (hsl[1] <= 100));
+ assert ((hsl[2] >= 0) && (hsl[2] <= 100));
+ }
+
+ /**
+ * Convert a HSL color to RGB.
+ *
+ * @param hue hue, between 0 and 359
+ * @param sat saturation, between 0 and 100
+ * @param lum luminance, between 0 and 100
+ * @return the rgb color as 0x00RRGGBB
+ */
+ private int hslToRgb(final int hue, final int sat, final int lum) {
+ assert ((hue >= 0) && (hue <= 360));
+ assert ((sat >= 0) && (sat <= 100));
+ assert ((lum >= 0) && (lum <= 100));
+
+ double S = sat / 100.0;
+ double L = lum / 100.0;
+ double C = (1.0 - Math.abs((2.0 * L) - 1.0)) * S;
+ double Hp = hue / 60.0;
+ double X = C * (1.0 - Math.abs((Hp % 2) - 1.0));
+ double Rp = 0.0;
+ double Gp = 0.0;
+ double Bp = 0.0;
+ if (Hp <= 1.0) {
+ Rp = C;
+ Gp = X;
+ } else if (Hp <= 2.0) {
+ Rp = X;
+ Gp = C;
+ } else if (Hp <= 3.0) {
+ Gp = C;
+ Bp = X;
+ } else if (Hp <= 4.0) {
+ Gp = X;
+ Bp = C;
+ } else if (Hp <= 5.0) {
+ Rp = X;
+ Bp = C;
+ } else if (Hp <= 6.0) {
+ Rp = C;
+ Bp = X;
+ }
+ double m = L - (C / 2.0);
+ int red = ((int) ((Rp + m) * 255.0)) << 16;
+ int green = ((int) ((Gp + m) * 255.0)) << 8;
+ int blue = (int) ((Bp + m) * 255.0);
+
+ return (red | green | blue);
+ }
+
+ /**
+ * Create the sixel palette.
+ */
+ 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 sixelPaletteSize colors for everything, and
+ // map the BufferedImage colors to their nearest neighbor in RGB
+ // space.
+
+ if (sixelPaletteSize == 2) {
+ rgbColors.add(0);
+ rgbColors.add(0xFFFFFF);
+ rgbSortedIndex[0] = 0;
+ rgbSortedIndex[1] = 1;
+ return;
+ }
+
+ // We build a palette using the Hue-Saturation-Luminence model,
+ // with 5+ bits for Hue, 2+ bits for Saturation, and 1+ bit for
+ // Luminance. We convert these colors to 24-bit RGB, sort them
+ // ascending, and steal the first index for pure black and the
+ // last for pure white. The 8-bit final palette favors bright
+ // colors, somewhere between pastel and classic television
+ // technicolor. 9- and 10-bit palettes are more uniform.
+
+ // Default at 256 colors.
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 1;
+
+ assert (sixelPaletteSize >= 256);
+ assert ((sixelPaletteSize == 256)
+ || (sixelPaletteSize == 512)
+ || (sixelPaletteSize == 1024)
+ || (sixelPaletteSize == 2048));
+
+ switch (sixelPaletteSize) {
+ case 512:
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 2;
+ break;
+ case 1024:
+ hueBits = 5;
+ satBits = 2;
+ lumBits = 3;
+ break;
+ case 2048:
+ hueBits = 5;
+ satBits = 3;
+ lumBits = 3;
+ break;
+ }
+ hueStep = (int) (Math.pow(2, hueBits));
+ satStep = (int) (100 / Math.pow(2, satBits));
+ // 1 bit for luminance: 40 and 70.
+ int lumBegin = 40;
+ int lumStep = 30;
+ switch (lumBits) {
+ case 2:
+ // 2 bits: 20, 40, 60, 80
+ lumBegin = 20;
+ lumStep = 20;
+ break;
+ case 3:
+ // 3 bits: 8, 20, 32, 44, 56, 68, 80, 92
+ lumBegin = 8;
+ lumStep = 12;
+ break;
+ }
+
+ // System.err.printf("<html><body>\n");
+ // Hue is evenly spaced around the wheel.
+ hslColors = new ArrayList<ArrayList<ArrayList<ColorIdx>>>();
+
+ final boolean DEBUG = false;
+ ArrayList<Integer> rawRgbList = new ArrayList<Integer>();
+
+ for (int hue = 0; hue < (360 - (360 % hueStep));
+ hue += (360/hueStep)) {
+
+ ArrayList<ArrayList<ColorIdx>> satList = null;
+ satList = new ArrayList<ArrayList<ColorIdx>>();
+ hslColors.add(satList);
+
+ // Saturation is linearly spaced between pastel and pure.
+ for (int sat = satStep; sat <= 100; sat += satStep) {
+
+ ArrayList<ColorIdx> lumList = new ArrayList<ColorIdx>();
+ satList.add(lumList);
+
+ // Luminance brackets the pure color, but leaning toward
+ // lighter.
+ for (int lum = lumBegin; lum < 100; lum += lumStep) {
+ /*
+ System.err.printf("<font style = \"color:");
+ System.err.printf("hsl(%d, %d%%, %d%%)",
+ hue, sat, lum);
+ System.err.printf(";\">=</font>\n");
+ */
+ int rgbColor = hslToRgb(hue, sat, lum);
+ rgbColors.add(rgbColor);
+ ColorIdx colorIdx = new ColorIdx(rgbColor,
+ rgbColors.size() - 1);
+ lumList.add(colorIdx);
+
+ rawRgbList.add(rgbColor);
+ if (DEBUG) {
+ int red = (rgbColor >>> 16) & 0xFF;
+ int green = (rgbColor >>> 8) & 0xFF;
+ int blue = rgbColor & 0xFF;
+ int [] backToHsl = new int[3];
+ rgbToHsl(red, green, blue, backToHsl);
+ System.err.printf("%d [%d] %d [%d] %d [%d]\n",
+ hue, backToHsl[0], sat, backToHsl[1],
+ lum, backToHsl[2]);
+ }
+ }
+ }
+ }
+ // System.err.printf("\n</body></html>\n");
+
+ assert (rgbColors.size() == sixelPaletteSize);
+
+ /*
+ * We need to sort rgbColors, so that toSixel() can know where
+ * BLACK and WHITE are in it. But we also need to be able to
+ * find the sorted values using the old unsorted indexes. So we
+ * will sort it, put all the indexes into a HashMap, and then
+ * build rgbSortedIndex[].
+ */
+ Collections.sort(rgbColors);
+ HashMap<Integer, Integer> rgbColorIndices = null;
+ rgbColorIndices = new HashMap<Integer, Integer>();
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ rgbColorIndices.put(rgbColors.get(i), 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 < sixelPaletteSize; i++) {
+ assert (rawRgbList != null);
+ int idx = rgbSortedIndex[i];
+ int rgbColor = rgbColors.get(idx);
+ if ((idx != 0) && (idx != sixelPaletteSize - 1)) {
+ /*
+ System.err.printf("%d %06x --> %d %06x\n",
+ i, rawRgbList.get(i), idx, rgbColors.get(idx));
+ */
+ assert (rgbColor == rawRgbList.get(i));
+ }
+ }
+ }
+
+ // Set the dimmest color as true black, and the brightest as true
+ // white.
+ rgbColors.set(0, 0);
+ rgbColors.set(sixelPaletteSize - 1, 0xFFFFFF);
+
+ /*
+ System.err.printf("<html><body>\n");
+ for (Integer rgb: rgbColors) {
+ System.err.printf("<font style = \"color:");
+ System.err.printf("#%06x", rgb);
+ System.err.printf(";\">=</font>\n");
+ }
+ System.err.printf("\n</body></html>\n");
+ */
+
+ }
+
+ /**
+ * Emit the sixel palette.
+ *
+ * @param sb the StringBuilder to append to
+ * @param used array of booleans set to true for each color actually
+ * used in this cell, or null to emit the entire palette
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ public String emitPalette(final StringBuilder sb,
+ final boolean [] used) {
+
+ 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,
+ ((rgbColor >>> 16) & 0xFF) * 100 / 255,
+ ((rgbColor >>> 8) & 0xFF) * 100 / 255,
+ ( rgbColor & 0xFF) * 100 / 255));
+ }
+ }
+ return sb.toString();
+ }
+ }
+
+ /**
+ * ImageCache is a least-recently-used cache that hangs on to the
+ * post-rendered sixel or iTerm2 string for a particular set of cells.
+ */
+ private class ImageCache {
+
+ /**
+ * Maximum size of the cache.
+ */
+ private int maxSize = 100;
+
+ /**
+ * The entries stored in the cache.
+ */
+ private HashMap<String, CacheEntry> cache = null;
+
+ /**
+ * CacheEntry is one entry in the cache.
+ */
+ private class CacheEntry {
+ /**
+ * The cache key.
+ */
+ public String key;
+
+ /**
+ * The cache data.
+ */
+ public String data;
+
+ /**
+ * The last time this entry was used.
+ */
+ public long millis = 0;
+
+ /**
+ * Public constructor.
+ *
+ * @param key the cache entry key
+ * @param data the cache entry data
+ */
+ public CacheEntry(final String key, final String data) {
+ this.key = key;
+ this.data = data;
+ this.millis = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param maxSize the maximum size of the cache
+ */
+ public ImageCache(final int maxSize) {
+ this.maxSize = maxSize;
+ cache = new HashMap<String, CacheEntry>();
+ }
+
+ /**
+ * Make a unique key for a list of cells.
+ *
+ * @param cells the cells
+ * @return the key
+ */
+ private String makeKey(final ArrayList<Cell> cells) {
+ StringBuilder sb = new StringBuilder();
+ for (Cell cell: cells) {
+ sb.append(cell.hashCode());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get an entry from the cache.
+ *
+ * @param cells the list of cells that are the cache key
+ * @return the sixel string representing these cells, or null if this
+ * list of cells is not in the cache
+ */
+ public String get(final ArrayList<Cell> cells) {
+ CacheEntry entry = cache.get(makeKey(cells));
+ if (entry == null) {
+ return null;
+ }
+ entry.millis = System.currentTimeMillis();
+ return entry.data;
+ }
+
+ /**
+ * Put an entry into the cache.
+ *
+ * @param cells the list of cells that are the cache key
+ * @param data the sixel string representing these cells
+ */
+ public void put(final ArrayList<Cell> cells, final String data) {
+ String key = makeKey(cells);
+
+ // System.err.println("put() " + key + " size " + cache.size());
+
+ assert (!cache.containsKey(key));
+
+ assert (cache.size() <= maxSize);
+ if (cache.size() == maxSize) {
+ // Cache is at limit, evict oldest entry.
+ long oldestTime = Long.MAX_VALUE;
+ String keyToRemove = null;
+ for (CacheEntry entry: cache.values()) {
+ if ((entry.millis < oldestTime) || (keyToRemove == null)) {
+ keyToRemove = entry.key;
+ oldestTime = entry.millis;
+ }
+ }
+ /*
+ System.err.println("put() remove key = " + keyToRemove +
+ " size " + cache.size());
+ */
+ assert (keyToRemove != null);
+ cache.remove(keyToRemove);
+ /*
+ System.err.println("put() removed, size " + cache.size());
+ */
+ }
+ assert (cache.size() <= maxSize);
+ CacheEntry entry = new CacheEntry(key, data);
+ assert (key.equals(entry.key));
+ cache.put(key, entry);
+ /*
+ System.err.println("put() added key " + key + " " +
+ " size " + cache.size());
+ */
+ }
+
+ }
+
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
/**
- * 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.
// 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();
+ }
}
/**
* 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.
"UTF-8"));
}
+ // Request Device Attributes
+ this.output.printf("\033[c");
+
+ // Request xterm report window/cell dimensions in pixels
+ this.output.printf("%s", xtermReportPixelDimensions());
+
// 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());
+
// Query the screen size
sessionInfo.queryWindowSize();
setDimensions(sessionInfo.getWindowWidth(),
windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
- // Permit RGB colors only if externally requested
- if (System.getProperty("jexer.ECMA48.rgbColor") != null) {
- if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
- doRgbColor = true;
- } else {
- doRgbColor = false;
- }
- }
+ reloadOptions();
// Spin up the input reader
- eventQueue = new LinkedList<TInputEvent>();
+ eventQueue = new ArrayList<TInputEvent>();
readerThread = new Thread(this);
readerThread.start();
this.output = writer;
+ // Request Device Attributes
+ this.output.printf("\033[c");
+
+ // Request xterm report window/cell dimensions in pixels
+ this.output.printf("%s", xtermReportPixelDimensions());
+
// 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());
+
// Query the screen size
sessionInfo.queryWindowSize();
setDimensions(sessionInfo.getWindowWidth(),
windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight());
- // Permit RGB colors only if externally requested
- if (System.getProperty("jexer.ECMA48.rgbColor") != null) {
- if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) {
- doRgbColor = true;
- } else {
- doRgbColor = false;
- }
- }
+ reloadOptions();
// Spin up the input reader
- eventQueue = new LinkedList<TInputEvent>();
+ eventQueue = new ArrayList<TInputEvent>();
readerThread = new Thread(this);
readerThread.start();
*/
@Override
public void flushPhysical() {
- String result = flushString();
+ StringBuilder sb = new StringBuilder();
if ((cursorVisible)
&& (cursorY >= 0)
&& (cursorX >= 0)
&& (cursorY <= height - 1)
&& (cursorX <= width - 1)
) {
- result += cursor(true);
- result += gotoXY(cursorX, cursorY);
+ flushString(sb);
+ sb.append(cursor(true));
+ sb.append(gotoXY(cursorX, cursorY));
} else {
- result += cursor(false);
+ sb.append(cursor(false));
+ flushString(sb);
}
- output.write(result);
+ output.write(sb.toString());
flush();
}
+ /**
+ * Resize the physical screen to match the logical screen dimensions.
+ */
+ @Override
+ public void resizeToScreen() {
+ // Send dtterm/xterm sequences, which will probably not work because
+ // allowWindowOps is defaulted to false.
+ String resizeString = String.format("\033[8;%d;%dt", getHeight(),
+ getWidth());
+ this.output.write(resizeString);
+ this.output.flush();
+ }
+
// ------------------------------------------------------------------------
// TerminalReader ---------------------------------------------------------
// ------------------------------------------------------------------------
*/
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;
try {
readerThread.join();
} catch (InterruptedException e) {
- e.printStackTrace();
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
}
- // Disable mouse reporting and show cursor
- output.printf("%s%s%s", mouse(false), cursor(true), normal());
- output.flush();
+ // Disable mouse reporting and show cursor. Defensive null check
+ // here in case closeTerminal() is called twice.
+ if (output != null) {
+ output.printf("%s%s%s%s", mouse(false), cursor(true),
+ defaultColor(), xtermResetSixelSettings());
+ output.flush();
+ }
if (setRawMode) {
sttyCooked();
} else {
// Shut down the streams, this should wake up the reader thread
// and make it exit.
- try {
- if (input != null) {
+ if (input != null) {
+ try {
input.close();
- input = null;
- }
- if (output != null) {
- output.close();
- output = null;
+ } catch (IOException e) {
+ // SQUASH
}
- } catch (IOException e) {
- e.printStackTrace();
+ input = null;
+ }
+ if (output != null) {
+ output.close();
+ output = null;
}
}
}
this.listener = listener;
}
+ /**
+ * Reload options from System properties.
+ */
+ public void reloadOptions() {
+ // Permit RGB colors only if externally requested.
+ if (System.getProperty("jexer.ECMA48.rgbColor",
+ "false").equals("true")
+ ) {
+ doRgbColor = true;
+ } else {
+ doRgbColor = false;
+ }
+
+ // Default to using images 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
+ }
+
+ // Default to using images for full-width characters.
+ if (System.getProperty("jexer.ECMA48.iTerm2Images",
+ "false").equals("true")) {
+ iterm2Images = true;
+ } else {
+ iterm2Images = false;
+ }
+
+ // Set custom colors
+ setCustomSystemColors();
+ }
+
// ------------------------------------------------------------------------
// Runnable ---------------------------------------------------------------
// ------------------------------------------------------------------------
// 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 {
events.clear();
}
+ if (output.checkError()) {
+ // This is EOF.
+ done = true;
+ }
+
// Wait 20 millis for more data
Thread.sleep(20);
}
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();
}
// ECMA48Terminal ---------------------------------------------------------
// ------------------------------------------------------------------------
+ /**
+ * Get the width of a character cell in pixels.
+ *
+ * @return the width in pixels of a character cell
+ */
+ public int getTextWidth() {
+ return (widthPixels / sessionInfo.getWindowWidth());
+ }
+
+ /**
+ * Get the height of a character cell in pixels.
+ *
+ * @return the height in pixels of a character cell
+ */
+ public int getTextHeight() {
+ return (heightPixels / sessionInfo.getWindowHeight());
+ }
+
/**
* Getter for sessionInfo.
*
process.waitFor();
break;
} catch (InterruptedException e) {
- e.printStackTrace();
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
}
}
int rc = process.exitValue();
// DEBUG
// reallyCleared = true;
+ boolean hasImage = false;
+
for (int x = 0; x < width; x++) {
Cell lCell = logical[x][y];
Cell pCell = physical[x][y];
return;
}
+ // Image cell: bypass the rest of the loop, it is not
+ // rendered here.
+ if ((wideCharImages && lCell.isImage())
+ || (!wideCharImages
+ && lCell.isImage()
+ && (lCell.getWidth() == Cell.Width.SINGLE))
+ ) {
+ hasImage = true;
+
+ // Save the last rendered cell
+ lastX = x;
+
+ // Physical is always updated
+ physical[x][y].setTo(lCell);
+ continue;
+ }
+
+ 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));
+ }
+
// Now emit only the modified attributes
if ((lCell.getForeColor() != lastAttr.getForeColor())
&& (lCell.getBackColor() != lastAttr.getBackColor())
}
// 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;
* Render the screen to a string that can be emitted to something that
* knows how to process ECMA-48/ANSI X3.64 escape sequences.
*
+ * @param sb StringBuilder to write escape sequences to
* @return escape sequences string that provides the updates to the
* physical screen
*/
- private String flushString() {
+ private String flushString(final StringBuilder sb) {
CellAttributes attr = null;
- StringBuilder sb = new StringBuilder();
if (reallyCleared) {
attr = new CellAttributes();
sb.append(clearAll());
}
+ /*
+ * For images support, draw all of the image output first, and then
+ * draw everything else afterwards. This works OK, but performance
+ * is still a drag on larger pictures.
+ */
for (int y = 0; y < height; y++) {
- flushLine(y, sb, attr);
+ for (int x = 0; x < width; x++) {
+ // If physical had non-image data that is now image data, the
+ // entire row must be redrawn.
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+ if (lCell.isImage() && !pCell.isImage()) {
+ unsetImageRow(y);
+ break;
+ }
+ }
}
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
- reallyCleared = false;
+ if (!lCell.isImage()
+ || (!wideCharImages
+ && (lCell.getWidth() != Cell.Width.SINGLE))
+ ) {
+ continue;
+ }
- String result = sb.toString();
- if (debugToStderr) {
- System.err.printf("flushString(): %s\n", result);
- }
- return result;
- }
+ int left = x;
+ int right = x;
+ while ((right < width)
+ && (logical[right][y].isImage())
+ && (!logical[right][y].equals(physical[right][y])
+ || reallyCleared)
+ ) {
+ right++;
+ }
+ ArrayList<Cell> cellsToDraw = new ArrayList<Cell>();
+ for (int i = 0; i < (right - x); i++) {
+ assert (logical[x + i][y].isImage());
+ cellsToDraw.add(logical[x + i][y]);
+
+ // Physical is always updated.
+ physical[x + i][y].setTo(lCell);
+ }
+ if (cellsToDraw.size() > 0) {
+ if (iterm2Images) {
+ sb.append(toIterm2Image(x, y, cellsToDraw));
+ } else if (jexerImages) {
+ sb.append(toJexerImage(x, y, cellsToDraw));
+ } else {
+ sb.append(toSixel(x, y, cellsToDraw));
+ }
+ }
+
+ x = right;
+ }
+ }
+
+ // Draw the text part now.
+ for (int y = 0; y < height; y++) {
+ flushLine(y, sb, attr);
+ }
+
+ reallyCleared = false;
+
+ String result = sb.toString();
+ if (debugToStderr) {
+ System.err.printf("flushString(): %s\n", result);
+ }
+ return result;
+ }
/**
* Reset keyboard/mouse input parser.
params = new ArrayList<String>();
params.clear();
params.add("");
+ decPrivateModeFlag = false;
}
/**
// Check for new window size
long windowSizeDelay = nowTime - windowSizeTime;
if (windowSizeDelay > 1000) {
+ int oldTextWidth = getTextWidth();
+ int oldTextHeight = getTextHeight();
+
sessionInfo.queryWindowSize();
int newWidth = sessionInfo.getWindowWidth();
int newHeight = sessionInfo.getWindowHeight();
|| (newHeight != windowResize.getHeight())
) {
+ // Request xterm report window dimensions in pixels again.
+ // Between now and then, ensure that the reported text cell
+ // size is the same by setting widthPixels and heightPixels
+ // to match the new dimensions.
+ widthPixels = oldTextWidth * newWidth;
+ heightPixels = oldTextHeight * newHeight;
+
if (debugToStderr) {
System.err.println("Screen size changed, old size " +
windowResize);
System.err.println(" new size " +
newWidth + " x " + newHeight);
+ System.err.println(" old pixels " +
+ oldTextWidth + " x " + oldTextHeight);
+ System.err.println(" new pixels " +
+ getTextWidth() + " x " + getTextHeight());
}
+ this.output.printf("%s", xtermReportPixelDimensions());
+ this.output.flush();
+
TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN,
newWidth, newHeight);
windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN,
// Mouse position, SGR (1006) coordinates
state = ParseState.MOUSE_SGR;
return;
+ case '?':
+ // DEC private mode flag
+ decPrivateModeFlag = true;
+ return;
default:
break;
}
events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
resetParser();
return;
+ case 'c':
+ // Device Attributes
+ if (decPrivateModeFlag == false) {
+ break;
+ }
+ for (String x: params) {
+ if (x.equals("4")) {
+ // Terminal reports sixel support
+ if (debugToStderr) {
+ System.err.println("Device Attributes: sixel");
+ }
+ }
+ if (x.equals("444")) {
+ // Terminal reports Jexer images support
+ if (debugToStderr) {
+ System.err.println("Device Attributes: Jexer images");
+ }
+ jexerImages = true;
+ }
+ }
+ return;
+ case 't':
+ // windowOps
+ if ((params.size() > 2) && (params.get(0).equals("4"))) {
+ if (debugToStderr) {
+ System.err.printf("windowOp pixels: " +
+ "height %s width %s\n",
+ params.get(1), params.get(2));
+ }
+ try {
+ widthPixels = Integer.parseInt(params.get(2));
+ heightPixels = Integer.parseInt(params.get(1));
+ } catch (NumberFormatException e) {
+ if (debugToStderr) {
+ e.printStackTrace();
+ }
+ }
+ if (widthPixels <= 0) {
+ widthPixels = 640;
+ }
+ if (heightPixels <= 0) {
+ 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:
break;
}
return;
}
+ /**
+ * Request (u)xterm to use the sixel settings we need:
+ *
+ * - enable sixel scrolling
+ *
+ * - disable private color registers (so that we can use one common
+ * palette)
+ *
+ * @return the string to emit to xterm
+ */
+ private String xtermSetSixelSettings() {
+ return "\033[?80h\033[?1070l";
+ }
+
+ /**
+ * Restore (u)xterm its default sixel settings:
+ *
+ * - enable sixel scrolling
+ *
+ * - enable private color registers
+ *
+ * @return the string to emit to xterm
+ */
+ private String xtermResetSixelSettings() {
+ return "\033[?80h\033[?1070h";
+ }
+
+ /**
+ * Request (u)xterm to report the current window and cell size dimensions
+ * in pixels.
+ *
+ * @return the string to emit to xterm
+ */
+ private String xtermReportPixelDimensions() {
+ // We will ask for both window and text cell dimensions, and
+ // hopefully one of them will work.
+ return "\033[14t\033[16t";
+ }
+
/**
* Tell (u)xterm that we want alt- keystrokes to send escape + character
* rather than set the 8th bit. Anyone who wants UTF8 should want this
return "\033]2;" + title + "\007";
}
+ // ------------------------------------------------------------------------
+ // 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.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String startSixel(final int x, final int y) {
+ StringBuilder sb = new StringBuilder();
+
+ assert (sixel == true);
+
+ // Place the cursor
+ sb.append(gotoXY(x, y));
+
+ // DCS
+ sb.append("\033Pq");
+
+ if (palette == null) {
+ palette = new SixelPalette();
+ // TODO: make this an option (shared palette or not)
+ palette.emitPalette(sb, null);
+ }
+
+ return sb.toString();
+ }
+
+ /**
+ * End a sixel string for display one row's worth of bitmap data.
+ *
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String endSixel() {
+ assert (sixel == true);
+
+ // ST
+ return ("\033\\");
+ }
+
+ /**
+ * Create a sixel string representing a row of several cells containing
+ * bitmap data.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param cells the cells containing the bitmap data
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String toSixel(final int x, final int y,
+ final ArrayList<Cell> cells) {
+
+ StringBuilder sb = new StringBuilder();
+
+ assert (cells != null);
+ assert (cells.size() > 0);
+ assert (cells.get(0).getImage() != null);
+
+ if (sixel == false) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ 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.
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int j = 0; j < cells.size(); j++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ if (sixelCache == null) {
+ sixelCache = new ImageCache(height * 10);
+ }
+
+ // Save and get rows to/from the cache that do NOT have inverted
+ // cells.
+ boolean saveInCache = true;
+ for (Cell cell: cells) {
+ if (cell.isInvertedImage()) {
+ saveInCache = false;
+ }
+ }
+ if (saveInCache) {
+ String cachedResult = sixelCache.get(cells);
+ if (cachedResult != null) {
+ // System.err.println("CACHE HIT");
+ sb.append(startSixel(x, y));
+ sb.append(cachedResult);
+ sb.append(endSixel());
+ return sb.toString();
+ }
+ // System.err.println("CACHE MISS");
+ }
+
+ 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.
+ int totalWidth = 0;
+ int fullWidth = cells.size() * getTextWidth();
+ int fullHeight = getTextHeight();
+ for (int i = 0; i < cells.size(); i++) {
+ totalWidth += cells.get(i).getImage().getWidth();
+ }
+
+ BufferedImage image = new BufferedImage(fullWidth,
+ fullHeight, BufferedImage.TYPE_INT_ARGB);
+
+ 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);
+
+ 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 {
+ 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);
+ }
+ }
+
+ /*
+ System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
+ i * imageWidth, 0, imageWidth, imageHeight,
+ 0, imageWidth);
+ System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
+ fullWidth, fullHeight, cells.size(), getTextWidth());
+ */
+
+ 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;
+ imageY++) {
+
+ image.setRGB(imageX, imageY, backgroundColor);
+ }
+ }
+ }
+ }
+ totalWidth -= ((cells.size() - 1) * imageWidth);
+ 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 {
+ 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);
+
+ if (totalWidth < getTextWidth()) {
+ int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
+
+ for (int imageX = image.getWidth() - totalWidth;
+ imageX < image.getWidth(); imageX++) {
+
+ for (int imageY = 0; imageY < fullHeight; imageY++) {
+ image.setRGB(imageX, imageY, backgroundColor);
+ }
+ }
+ }
+
+ // 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);
+ }
+ image = palette.ditherImage(image);
+
+ // Collect the raster information
+ 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;
+ }
+ }
+ palette.emitPalette(sb, usedColors);
+ */
+
+ // Render the entire row of cells.
+ for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
+ int [][] sixels = new int[image.getWidth()][6];
+
+ // See which colors are actually used in this band of sixels.
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int imageY = 0;
+ (imageY < 6) && (imageY + currentRow < fullHeight);
+ imageY++) {
+
+ int colorIdx = image.getRGB(imageX, imageY + currentRow);
+ assert (colorIdx >= 0);
+ assert (colorIdx < sixelPaletteSize);
+
+ sixels[imageX][imageY] = colorIdx;
+ }
+ }
+
+ 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++) {
+ if (sixels[imageX][j] == i) {
+ isUsed = true;
+ }
+ }
+ }
+ if (isUsed == false) {
+ continue;
+ }
+
+ // Set to the beginning of scan line for the next set of
+ // 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.
+ int data = 0;
+ for (int j = 0;
+ (j < 6) && (currentRow + j < fullHeight);
+ j++) {
+
+ if (sixels[imageX][j] == i) {
+ switch (j) {
+ case 0:
+ data += 1;
+ break;
+ case 1:
+ data += 2;
+ break;
+ case 2:
+ data += 4;
+ break;
+ case 3:
+ data += 8;
+ break;
+ case 4:
+ data += 16;
+ break;
+ case 5:
+ data += 32;
+ break;
+ }
+ if ((currentRow + j + 1) > rasterHeight) {
+ rasterHeight = currentRow + j + 1;
+ }
+ }
+ }
+ assert (data >= 0);
+ assert (data < 64);
+ data += 63;
+
+ 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++)
+
+ // 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("-");
+
+ } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
+
+ // Kill the very last "-", because it is unnecessary.
+ sb.deleteCharAt(sb.length() - 1);
+
+ // Add the raster information
+ sb.insert(0, String.format("\"1;1;%d;%d", rasterWidth, rasterHeight));
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ sixelCache.put(cells, sb.toString());
+ }
+
+ 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 -----------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // iTerm2 image output support --------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create an iTerm2 images string representing a row of several cells
+ * containing bitmap data.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param cells the cells containing the bitmap data
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String toIterm2Image(final int x, final int y,
+ final ArrayList<Cell> cells) {
+
+ StringBuilder sb = new StringBuilder();
+
+ assert (cells != null);
+ assert (cells.size() > 0);
+ assert (cells.get(0).getImage() != null);
+
+ if (iterm2Images == false) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ 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
+ // cells.
+ boolean saveInCache = true;
+ for (Cell cell: cells) {
+ if (cell.isInvertedImage()) {
+ saveInCache = false;
+ }
+ }
+ if (saveInCache) {
+ String cachedResult = iterm2Cache.get(cells);
+ if (cachedResult != null) {
+ // System.err.println("CACHE HIT");
+ sb.append(gotoXY(x, y));
+ sb.append(cachedResult);
+ return sb.toString();
+ }
+ // System.err.println("CACHE MISS");
+ }
+
+ 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.
+ int totalWidth = 0;
+ int fullWidth = cells.size() * getTextWidth();
+ int fullHeight = getTextHeight();
+ for (int i = 0; i < cells.size(); i++) {
+ totalWidth += cells.get(i).getImage().getWidth();
+ }
+
+ BufferedImage image = new BufferedImage(fullWidth,
+ fullHeight, BufferedImage.TYPE_INT_ARGB);
+
+ 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);
+ 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 {
+ 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);
+ }
+ }
+
+ /*
+ System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
+ i * imageWidth, 0, imageWidth, imageHeight,
+ 0, imageWidth);
+ System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
+ fullWidth, fullHeight, cells.size(), getTextWidth());
+ */
+
+ 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;
+ imageY++) {
+
+ image.setRGB(imageX, imageY, backgroundColor);
+ }
+ }
+ }
+ }
+ totalWidth -= ((cells.size() - 1) * imageWidth);
+ 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 {
+ 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);
+
+ if (totalWidth < getTextWidth()) {
+ int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
+
+ for (int imageX = image.getWidth() - totalWidth;
+ imageX < image.getWidth(); imageX++) {
+
+ for (int imageY = 0; imageY < fullHeight; imageY++) {
+ image.setRGB(imageX, imageY, backgroundColor);
+ }
+ }
+ }
+
+ /*
+ * From https://iterm2.com/documentation-images.html:
+ *
+ * Protocol
+ *
+ * iTerm2 extends the xterm protocol with a set of proprietary escape
+ * sequences. In general, the pattern is:
+ *
+ * ESC ] 1337 ; key = value ^G
+ *
+ * Whitespace is shown here for ease of reading: in practice, no
+ * spaces should be used.
+ *
+ * For file transfer and inline images, the code is:
+ *
+ * ESC ] 1337 ; File = [optional arguments] : base-64 encoded file contents ^G
+ *
+ * The optional arguments are formatted as key=value with a semicolon
+ * between each key-value pair. They are described below:
+ *
+ * Key Description of value
+ * name base-64 encoded filename. Defaults to "Unnamed file".
+ * size File size in bytes. Optional; this is only used by the
+ * progress indicator.
+ * width Width to render. See notes below.
+ * height Height to render. See notes below.
+ * preserveAspectRatio If set to 0, then the image's inherent aspect
+ * ratio will not be respected; otherwise, it
+ * will fill the specified width and height as
+ * much as possible without stretching. Defaults
+ * to 1.
+ * inline If set to 1, the file will be displayed inline. Otherwise,
+ * it will be downloaded with no visual representation in the
+ * terminal session. Defaults to 0.
+ *
+ * The width and height are given as a number followed by a unit, or
+ * the word "auto".
+ *
+ * N: N character cells.
+ * Npx: N pixels.
+ * N%: N percent of the session's width or height.
+ * auto: The image's inherent size will be used to determine an
+ * appropriate dimension.
+ *
+ */
+
+ // File contents can be several image formats. We will use PNG.
+ ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream(1024);
+ try {
+ if (!ImageIO.write(image.getSubimage(0, 0, image.getWidth(),
+ Math.min(image.getHeight(), fullHeight)),
+ "PNG", pngOutputStream)
+ ) {
+ // We failed to render image, bail out.
+ return "";
+ }
+ } catch (IOException e) {
+ // We failed to render image, bail out.
+ 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;",
+ cells.size()));
+ */
+ /*
+ sb.append(String.format("width=$dpx;height=%dpx;preserveAspectRatio=1;",
+ image.getWidth(), Math.min(image.getHeight(),
+ getTextHeight())));
+ */
+ sb.append("inline=1:");
+ sb.append(base64.encodeToString(pngOutputStream.toByteArray()));
+ sb.append("\007");
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ iterm2Cache.put(cells, sb.toString());
+ }
+
+ return (gotoXY(x, y) + sb.toString());
+ }
+
+ /**
+ * Get the iTerm2 images support flag.
+ *
+ * @return true if this terminal is emitting iTerm2 images
+ */
+ public boolean hasIterm2Images() {
+ return iterm2Images;
+ }
+
+ // ------------------------------------------------------------------------
+ // End iTerm2 image output support ----------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // Jexer image output support ---------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create a Jexer images string representing a row of several cells
+ * containing bitmap data.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param cells the cells containing the bitmap data
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String toJexerImage(final int x, final int y,
+ final ArrayList<Cell> cells) {
+
+ StringBuilder sb = new StringBuilder();
+
+ assert (cells != null);
+ assert (cells.size() > 0);
+ assert (cells.get(0).getImage() != null);
+
+ if (jexerImages == false) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ 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
+ // cells.
+ boolean saveInCache = true;
+ for (Cell cell: cells) {
+ if (cell.isInvertedImage()) {
+ saveInCache = false;
+ }
+ }
+ if (saveInCache) {
+ String cachedResult = jexerCache.get(cells);
+ if (cachedResult != null) {
+ // System.err.println("CACHE HIT");
+ sb.append(gotoXY(x, y));
+ sb.append(cachedResult);
+ return sb.toString();
+ }
+ // System.err.println("CACHE MISS");
+ }
+
+ 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.
+ int totalWidth = 0;
+ int fullWidth = cells.size() * getTextWidth();
+ int fullHeight = getTextHeight();
+ for (int i = 0; i < cells.size(); i++) {
+ totalWidth += cells.get(i).getImage().getWidth();
+ }
+
+ BufferedImage image = new BufferedImage(fullWidth,
+ fullHeight, BufferedImage.TYPE_INT_ARGB);
+
+ 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);
+ 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 {
+ 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);
+ }
+ }
+
+ /*
+ System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
+ i * imageWidth, 0, imageWidth, imageHeight,
+ 0, imageWidth);
+ System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
+ fullWidth, fullHeight, cells.size(), getTextWidth());
+ */
+
+ 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;
+ imageY++) {
+
+ image.setRGB(imageX, imageY, backgroundColor);
+ }
+ }
+ }
+ }
+ totalWidth -= ((cells.size() - 1) * imageWidth);
+ 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 {
+ 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);
+
+ if (totalWidth < getTextWidth()) {
+ int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
+
+ for (int imageX = image.getWidth() - totalWidth;
+ imageX < image.getWidth(); imageX++) {
+
+ for (int imageY = 0; imageY < fullHeight; imageY++) {
+ image.setRGB(imageX, imageY, backgroundColor);
+ }
+ }
+ }
+
+ sb.append(String.format("\033]444;%d;%d;0;", image.getWidth(),
+ Math.min(image.getHeight(), fullHeight)));
+
+ byte [] bytes = new byte[image.getWidth() * image.getHeight() * 3];
+ int stride = image.getWidth();
+ for (int px = 0; px < stride; px++) {
+ for (int py = 0; py < image.getHeight(); py++) {
+ int rgb = image.getRGB(px, py);
+ bytes[(py * stride * 3) + (px * 3)] = (byte) ((rgb >>> 16) & 0xFF);
+ bytes[(py * stride * 3) + (px * 3) + 1] = (byte) ((rgb >>> 8) & 0xFF);
+ bytes[(py * stride * 3) + (px * 3) + 2] = (byte) ( rgb & 0xFF);
+ }
+ }
+ sb.append(base64.encodeToString(bytes));
+ sb.append("\007");
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ jexerCache.put(cells, sb.toString());
+ }
+
+ return (gotoXY(x, y) + sb.toString());
+ }
+
+ /**
+ * Get the Jexer images support flag.
+ *
+ * @return true if this terminal is emitting Jexer images
+ */
+ public boolean hasJexerImages() {
+ return jexerImages;
+ }
+
+ // ------------------------------------------------------------------------
+ // End Jexer image 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.
*
*/
private String colorRGB(final int colorRGB, final boolean foreground) {
- int colorRed = (colorRGB >> 16) & 0xFF;
- int colorGreen = (colorRGB >> 8) & 0xFF;
- int colorBlue = colorRGB & 0xFF;
+ int colorRed = (colorRGB >>> 16) & 0xFF;
+ int colorGreen = (colorRGB >>> 8) & 0xFF;
+ int colorBlue = colorRGB & 0xFF;
StringBuilder sb = new StringBuilder();
if (foreground) {
* e.g. "\033[42m"
*/
private String colorRGB(final int foreColorRGB, final int backColorRGB) {
- int foreColorRed = (foreColorRGB >> 16) & 0xFF;
- int foreColorGreen = (foreColorRGB >> 8) & 0xFF;
- int foreColorBlue = foreColorRGB & 0xFF;
- int backColorRed = (backColorRGB >> 16) & 0xFF;
- int backColorGreen = (backColorRGB >> 8) & 0xFF;
- int backColorBlue = backColorRGB & 0xFF;
+ int foreColorRed = (foreColorRGB >>> 16) & 0xFF;
+ int foreColorGreen = (foreColorRGB >>> 8) & 0xFF;
+ int foreColorBlue = foreColorRGB & 0xFF;
+ int backColorRed = (backColorRGB >>> 16) & 0xFF;
+ int backColorGreen = (backColorRGB >>> 8) & 0xFF;
+ int backColorBlue = backColorRGB & 0xFF;
StringBuilder sb = new StringBuilder();
sb.append(String.format("\033[38;2;%d;%d;%dm",
// 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) {
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");
final boolean bold, final boolean reverse, final boolean blink,
final boolean underline) {
- int foreColorRed = (foreColorRGB >> 16) & 0xFF;
- int foreColorGreen = (foreColorRGB >> 8) & 0xFF;
- int foreColorBlue = foreColorRGB & 0xFF;
- int backColorRed = (backColorRGB >> 16) & 0xFF;
- int backColorGreen = (backColorRGB >> 8) & 0xFF;
- int backColorBlue = backColorRGB & 0xFF;
+ int foreColorRed = (foreColorRGB >>> 16) & 0xFF;
+ int foreColorGreen = (foreColorRGB >>> 8) & 0xFF;
+ int foreColorBlue = foreColorRGB & 0xFF;
+ int backColorRed = (backColorRGB >>> 16) & 0xFF;
+ int backColorGreen = (backColorRGB >>> 8) & 0xFF;
+ int backColorBlue = backColorRGB & 0xFF;
StringBuilder sb = new StringBuilder();
if ( bold && reverse && blink && !underline ) {
}
/**
- * 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"
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.
*
*
* Note that this also sets the alternate/primary screen buffer.
*
+ * Finally, also emit a Privacy Message sequence that Jexer recognizes to
+ * mean "hide the mouse pointer." We have to use our own sequence to do
+ * this because there is no standard in xterm for unilaterally hiding the
+ * pointer all the time (regardless of typing).
+ *
* @param on If true, enable mouse report and use the alternate screen
* buffer. If false disable mouse reporting and use the primary screen
* buffer.
*/
private String mouse(final boolean on) {
if (on) {
- return "\033[?1002;1003;1005;1006h\033[?1049h";
+ return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\";
}
- return "\033[?1002;1003;1006;1005l\033[?1049l";
+ return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\";
}
}