X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fbackend%2FECMA48Terminal.java;fp=src%2Fjexer%2Fbackend%2FECMA48Terminal.java;h=0000000000000000000000000000000000000000;hb=36b0745bab5665306391440a531e1ee1c0625445;hp=e2997d2f6b17486356ddd9d902d114039258bdef;hpb=686d4da2d2ecc203d5f8b524225a4327777825be;p=fanfix.git diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java deleted file mode 100644 index e2997d2..0000000 --- a/src/jexer/backend/ECMA48Terminal.java +++ /dev/null @@ -1,4384 +0,0 @@ -/* - * Jexer - Java Text User Interface - * - * The MIT License (MIT) - * - * 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"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - * @author Kevin Lamonte [kevin.lamonte@gmail.com] - * @version 1 - */ -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.InputStreamReader; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -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 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.*; - -/** - * This class reads keystrokes and mouse events and emits output to ANSI - * X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc. - */ -public class ECMA48Terminal extends LogicalScreen - implements TerminalReader, Runnable { - - // ------------------------------------------------------------------------ - // Constants -------------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * States in the input parser. - */ - private enum ParseState { - GROUND, - ESCAPE, - ESCAPE_INTERMEDIATE, - CSI_ENTRY, - CSI_PARAM, - MOUSE, - MOUSE_SGR, - } - - // ------------------------------------------------------------------------ - // Variables -------------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Emit debugging to stderr. - */ - private boolean debugToStderr = false; - - /** - * If true, emit T.416-style RGB colors for normal system colors. This - * is a) expensive in bandwidth, and b) potentially terrible looking for - * non-xterms. - */ - private static boolean doRgbColor = false; - - /** - * The session information. - */ - private SessionInfo sessionInfo; - - /** - * The event queue, filled up by a thread reading on input. - */ - private List eventQueue; - - /** - * If true, we want the reader thread to exit gracefully. - */ - private boolean stopReaderThread; - - /** - * The reader thread. - */ - private Thread readerThread; - - /** - * Parameters being collected. E.g. if the string is \033[1;3m, then - * params[0] will be 1 and params[1] will be 3. - */ - private List params; - - /** - * Current parsing state. - */ - private ParseState state; - - /** - * The time we entered ESCAPE. If we get a bare escape without a code - * following it, this is used to return that bare escape. - */ - private long escapeTime; - - /** - * The time we last checked the window size. We try not to spawn stty - * more than once per second. - */ - private long windowSizeTime; - - /** - * true if mouse1 was down. Used to report mouse1 on the release event. - */ - private boolean mouse1; - - /** - * true if mouse2 was down. Used to report mouse2 on the release event. - */ - private boolean mouse2; - - /** - * true if mouse3 was down. Used to report mouse3 on the release event. - */ - private boolean mouse3; - - /** - * Cache the cursor visibility value so we only emit the sequence when we - * need to. - */ - private boolean cursorOn = true; - - /** - * Cache the last window size to figure out if a TResizeEvent needs to be - * generated. - */ - 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 = false; - - /** - * If true, '?' was seen in terminal response. - */ - private boolean decPrivateModeFlag = false; - - /** - * The terminal's input. If an InputStream is not specified in the - * constructor, then this InputStreamReader will be bound to System.in - * with UTF-8 encoding. - */ - private Reader input; - - /** - * The terminal's raw InputStream. If an InputStream is not specified in - * the constructor, then this InputReader will be bound to System.in. - * This is used by run() to see if bytes are available() before calling - * (Reader)input.read(). - */ - private InputStream inputStream; - - /** - * The terminal's output. If an OutputStream is not specified in the - * constructor, then this PrintWriter will be bound to System.out with - * UTF-8 encoding. - */ - private PrintWriter output; - - /** - * The listening object that run() wakes up on new input. - */ - 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 rgbColors = new ArrayList(); - - /** - * 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>> 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> 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 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("\n"); - // Hue is evenly spaced around the wheel. - hslColors = new ArrayList>>(); - - final boolean DEBUG = false; - ArrayList rawRgbList = new ArrayList(); - - for (int hue = 0; hue < (360 - (360 % hueStep)); - hue += (360/hueStep)) { - - ArrayList> satList = null; - satList = new ArrayList>(); - hslColors.add(satList); - - // Saturation is linearly spaced between pastel and pure. - for (int sat = satStep; sat <= 100; sat += satStep) { - - ArrayList lumList = new ArrayList(); - satList.add(lumList); - - // Luminance brackets the pure color, but leaning toward - // lighter. - for (int lum = lumBegin; lum < 100; lum += lumStep) { - /* - System.err.printf("=\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\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 rgbColorIndices = null; - rgbColorIndices = new HashMap(); - 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("\n"); - for (Integer rgb: rgbColors) { - System.err.printf("=\n"); - } - System.err.printf("\n\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 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(); - } - - /** - * Make a unique key for a list of cells. - * - * @param cells the cells - * @return the key - */ - private String makeKey(final ArrayList 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 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 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(). 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; 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. - * @param windowWidth the number of text columns to start with - * @param windowHeight the number of text rows to start with - * @throws UnsupportedEncodingException if an exception is thrown when - * creating the InputStreamReader - */ - public ECMA48Terminal(final Object listener, final InputStream input, - final OutputStream output, final int windowWidth, - final int windowHeight) throws UnsupportedEncodingException { - - this(listener, input, output); - - // Send dtterm/xterm sequences, which will probably not work because - // allowWindowOps is defaulted to false. - if ((windowWidth > 0) && (windowHeight > 0)) { - String resizeString = String.format("\033[8;%d;%dt", windowHeight, - windowWidth); - this.output.write(resizeString); - this.output.flush(); - } - } - - /** - * Constructor sets up state for getEvent(). - * - * @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; 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. - * @throws UnsupportedEncodingException if an exception is thrown when - * creating the InputStreamReader - */ - public ECMA48Terminal(final Object listener, final InputStream input, - final OutputStream output) throws UnsupportedEncodingException { - - resetParser(); - mouse1 = false; - mouse2 = false; - mouse3 = false; - stopReaderThread = false; - this.listener = listener; - - if (input == null) { - // inputStream = System.in; - inputStream = new FileInputStream(FileDescriptor.in); - sttyRaw(); - setRawMode = true; - } else { - inputStream = input; - } - this.input = new InputStreamReader(inputStream, "UTF-8"); - - if (input instanceof SessionInfo) { - // This is a TelnetInputStream that exposes window size and - // environment variables from the telnet layer. - sessionInfo = (SessionInfo) input; - } - if (sessionInfo == null) { - if (input == null) { - // Reading right off the tty - sessionInfo = new TTYSessionInfo(); - } else { - sessionInfo = new TSessionInfo(); - } - } - - if (output == null) { - this.output = new PrintWriter(new OutputStreamWriter(System.out, - "UTF-8")); - } else { - this.output = new PrintWriter(new OutputStreamWriter(output, - "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(), - sessionInfo.getWindowHeight()); - - // Hang onto the window size - windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, - sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); - - reloadOptions(); - - // Spin up the input reader - eventQueue = new ArrayList(); - readerThread = new Thread(this); - readerThread.start(); - - // Clear the screen - this.output.write(clearAll()); - this.output.flush(); - } - - /** - * Constructor sets up state for getEvent(). - * - * @param listener the object this backend needs to wake up when new - * input comes in - * @param input the InputStream underlying 'reader'. Its available() - * method is used to determine if reader.read() will block or not. - * @param reader a Reader connected to the remote user. - * @param writer a PrintWriter connected to the remote user. - * @param setRawMode if true, set System.in into raw mode with stty. - * This should in general not be used. It is here solely for Demo3, - * which uses System.in. - * @throws IllegalArgumentException if input, reader, or writer are null. - */ - public ECMA48Terminal(final Object listener, final InputStream input, - final Reader reader, final PrintWriter writer, - final boolean setRawMode) { - - if (input == null) { - throw new IllegalArgumentException("InputStream must be specified"); - } - if (reader == null) { - throw new IllegalArgumentException("Reader must be specified"); - } - if (writer == null) { - throw new IllegalArgumentException("Writer must be specified"); - } - resetParser(); - mouse1 = false; - mouse2 = false; - mouse3 = false; - stopReaderThread = false; - this.listener = listener; - - inputStream = input; - this.input = reader; - - if (setRawMode == true) { - sttyRaw(); - } - this.setRawMode = setRawMode; - - if (input instanceof SessionInfo) { - // This is a TelnetInputStream that exposes window size and - // environment variables from the telnet layer. - sessionInfo = (SessionInfo) input; - } - if (sessionInfo == null) { - if (setRawMode == true) { - // Reading right off the tty - sessionInfo = new TTYSessionInfo(); - } else { - sessionInfo = new TSessionInfo(); - } - } - - 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(), - sessionInfo.getWindowHeight()); - - // Hang onto the window size - windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, - sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); - - reloadOptions(); - - // Spin up the input reader - eventQueue = new ArrayList(); - readerThread = new Thread(this); - readerThread.start(); - - // Clear the screen - this.output.write(clearAll()); - this.output.flush(); - } - - /** - * Constructor sets up state for getEvent(). - * - * @param listener the object this backend needs to wake up when new - * input comes in - * @param input the InputStream underlying 'reader'. Its available() - * method is used to determine if reader.read() will block or not. - * @param reader a Reader connected to the remote user. - * @param writer a PrintWriter connected to the remote user. - * @throws IllegalArgumentException if input, reader, or writer are null. - */ - public ECMA48Terminal(final Object listener, final InputStream input, - final Reader reader, final PrintWriter writer) { - - this(listener, input, reader, writer, false); - } - - // ------------------------------------------------------------------------ - // LogicalScreen ---------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Set the window title. - * - * @param title the new title - */ - @Override - public void setTitle(final String title) { - output.write(getSetTitleString(title)); - flush(); - } - - /** - * Push the logical screen to the physical device. - */ - @Override - public void flushPhysical() { - StringBuilder sb = new StringBuilder(); - if ((cursorVisible) - && (cursorY >= 0) - && (cursorX >= 0) - && (cursorY <= height - 1) - && (cursorX <= width - 1) - ) { - flushString(sb); - sb.append(cursor(true)); - sb.append(gotoXY(cursorX, cursorY)); - } else { - sb.append(cursor(false)); - flushString(sb); - } - 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 --------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Check if there are events in the queue. - * - * @return if true, getEvents() has something to return to the backend - */ - public boolean hasEvents() { - synchronized (eventQueue) { - return (eventQueue.size() > 0); - } - } - - /** - * Return any events in the IO queue. - * - * @param queue list to append new events to - */ - public void getEvents(final List queue) { - synchronized (eventQueue) { - if (eventQueue.size() > 0) { - synchronized (queue) { - queue.addAll(eventQueue); - } - eventQueue.clear(); - } - } - } - - /** - * Restore terminal to normal state. - */ - public void closeTerminal() { - - // System.err.println("=== closeTerminal() ==="); System.err.flush(); - - // Tell the reader thread to stop looking at input - stopReaderThread = true; - try { - readerThread.join(); - } catch (InterruptedException e) { - if (debugToStderr) { - e.printStackTrace(); - } - } - - // 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(); - setRawMode = false; - // We don't close System.in/out - } else { - // Shut down the streams, this should wake up the reader thread - // and make it exit. - if (input != null) { - try { - input.close(); - } catch (IOException e) { - // SQUASH - } - input = null; - } - if (output != null) { - output.close(); - output = null; - } - } - } - - /** - * Set listener to a different Object. - * - * @param listener the new listening object that run() wakes up on new - * input - */ - public void setListener(final Object listener) { - 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 --------------------------------------------------------------- - // ------------------------------------------------------------------------ - - /** - * Read function runs on a separate thread. - */ - public void run() { - boolean done = false; - // available() will often return > 1, so we need to read in chunks to - // stay caught up. - char [] readBuffer = new char[128]; - List events = new ArrayList(); - - while (!done && !stopReaderThread) { - try { - // We assume that if inputStream has bytes available, then - // input won't block on read(). - int n = inputStream.available(); - - /* - System.err.printf("inputStream.available(): %d\n", n); - System.err.flush(); - */ - - if (n > 0) { - if (readBuffer.length < n) { - // The buffer wasn't big enough, make it huger - readBuffer = new char[readBuffer.length * 2]; - } - - // System.err.printf("BEFORE read()\n"); System.err.flush(); - - int rc = input.read(readBuffer, 0, readBuffer.length); - - /* - System.err.printf("AFTER read() %d\n", rc); - System.err.flush(); - */ - - if (rc == -1) { - // This is EOF - done = true; - } else { - for (int i = 0; i < rc; i++) { - int ch = readBuffer[i]; - processChar(events, (char)ch); - } - getIdleEvents(events); - if (events.size() > 0) { - // Add to the queue for the backend thread to - // be able to obtain. - synchronized (eventQueue) { - eventQueue.addAll(events); - } - if (listener != null) { - synchronized (listener) { - listener.notifyAll(); - } - } - events.clear(); - } - } - } else { - getIdleEvents(events); - if (events.size() > 0) { - synchronized (eventQueue) { - eventQueue.addAll(events); - } - if (listener != null) { - synchronized (listener) { - listener.notifyAll(); - } - } - events.clear(); - } - - if (output.checkError()) { - // This is EOF. - done = true; - } - - // Wait 20 millis for more data - Thread.sleep(20); - } - // System.err.println("end while loop"); System.err.flush(); - } catch (InterruptedException e) { - // SQUASH - } catch (IOException e) { - e.printStackTrace(); - 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. - * - * @return the SessionInfo - */ - public SessionInfo getSessionInfo() { - return sessionInfo; - } - - /** - * Get the output writer. - * - * @return the Writer - */ - public PrintWriter getOutput() { - return output; - } - - /** - * Call 'stty' to set cooked mode. - * - *

Actually executes '/bin/sh -c stty sane cooked < /dev/tty' - */ - private void sttyCooked() { - doStty(false); - } - - /** - * Call 'stty' to set raw mode. - * - *

Actually executes '/bin/sh -c stty -ignbrk -brkint -parmrk -istrip - * -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten - * -parenb cs8 min 1 < /dev/tty' - */ - private void sttyRaw() { - doStty(true); - } - - /** - * Call 'stty' to set raw or cooked mode. - * - * @param mode if true, set raw mode, otherwise set cooked mode - */ - private void doStty(final boolean mode) { - String [] cmdRaw = { - "/bin/sh", "-c", "stty -ignbrk -brkint -parmrk -istrip -inlcr -igncr -icrnl -ixon -opost -echo -echonl -icanon -isig -iexten -parenb cs8 min 1 < /dev/tty" - }; - String [] cmdCooked = { - "/bin/sh", "-c", "stty sane cooked < /dev/tty" - }; - try { - Process process; - if (mode) { - process = Runtime.getRuntime().exec(cmdRaw); - } else { - process = Runtime.getRuntime().exec(cmdCooked); - } - BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); - String line = in.readLine(); - if ((line != null) && (line.length() > 0)) { - System.err.println("WEIRD?! Normal output from stty: " + line); - } - while (true) { - BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream(), "UTF-8")); - line = err.readLine(); - if ((line != null) && (line.length() > 0)) { - System.err.println("Error output from stty: " + line); - } - try { - process.waitFor(); - break; - } catch (InterruptedException e) { - if (debugToStderr) { - e.printStackTrace(); - } - } - } - int rc = process.exitValue(); - if (rc != 0) { - System.err.println("stty returned error code: " + rc); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - /** - * Flush output. - */ - public void flush() { - output.flush(); - } - - /** - * Perform a somewhat-optimal rendering of a line. - * - * @param y row coordinate. 0 is the top-most row. - * @param sb StringBuilder to write escape sequences to - * @param lastAttr cell attributes from the last call to flushLine - */ - private void flushLine(final int y, final StringBuilder sb, - CellAttributes lastAttr) { - - int lastX = -1; - int textEnd = 0; - for (int x = 0; x < width; x++) { - Cell lCell = logical[x][y]; - if (!lCell.isBlank()) { - textEnd = x; - } - } - // Push textEnd to first column beyond the text area - textEnd++; - - // DEBUG - // reallyCleared = true; - - boolean hasImage = false; - - for (int x = 0; x < width; x++) { - Cell lCell = logical[x][y]; - Cell pCell = physical[x][y]; - - if (!lCell.equals(pCell) || reallyCleared) { - - if (debugToStderr) { - System.err.printf("\n--\n"); - System.err.printf(" Y: %d X: %d\n", y, x); - System.err.printf(" lCell: %s\n", lCell); - System.err.printf(" pCell: %s\n", pCell); - System.err.printf(" ==== \n"); - } - - if (lastAttr == null) { - lastAttr = new CellAttributes(); - sb.append(normal()); - } - - // Place the cell - if ((lastX != (x - 1)) || (lastX == -1)) { - // Advancing at least one cell, or the first gotoXY - sb.append(gotoXY(x, y)); - } - - assert (lastAttr != null); - - if ((x == textEnd) && (textEnd < width - 1)) { - assert (lCell.isBlank()); - - for (int i = x; i < width; i++) { - assert (logical[i][y].isBlank()); - // Physical is always updated - physical[i][y].reset(); - } - - // Clear remaining line - sb.append(clearRemainingLine()); - lastAttr.reset(); - 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()) - && (!lCell.isRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Both colors changed, attributes the same - sb.append(color(lCell.isBold(), - lCell.getForeColor(), lCell.getBackColor())); - - if (debugToStderr) { - System.err.printf("1 Change only fore/back colors\n"); - } - - } else if (lCell.isRGB() - && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB()) - && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Both colors changed, attributes the same - sb.append(colorRGB(lCell.getForeColorRGB(), - lCell.getBackColorRGB())); - - if (debugToStderr) { - System.err.printf("1 Change only fore/back colors (RGB)\n"); - } - } else if ((lCell.getForeColor() != lastAttr.getForeColor()) - && (lCell.getBackColor() != lastAttr.getBackColor()) - && (!lCell.isRGB()) - && (lCell.isBold() != lastAttr.isBold()) - && (lCell.isReverse() != lastAttr.isReverse()) - && (lCell.isUnderline() != lastAttr.isUnderline()) - && (lCell.isBlink() != lastAttr.isBlink()) - ) { - // Everything is different - sb.append(color(lCell.getForeColor(), - lCell.getBackColor(), - lCell.isBold(), lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - - if (debugToStderr) { - System.err.printf("2 Set all attributes\n"); - } - } else if ((lCell.getForeColor() != lastAttr.getForeColor()) - && (lCell.getBackColor() == lastAttr.getBackColor()) - && (!lCell.isRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - - // Attributes same, foreColor different - sb.append(color(lCell.isBold(), - lCell.getForeColor(), true)); - - if (debugToStderr) { - System.err.printf("3 Change foreColor\n"); - } - } else if (lCell.isRGB() - && (lCell.getForeColorRGB() != lastAttr.getForeColorRGB()) - && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB()) - && (lCell.getForeColorRGB() >= 0) - && (lCell.getBackColorRGB() >= 0) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Attributes same, foreColor different - sb.append(colorRGB(lCell.getForeColorRGB(), true)); - - if (debugToStderr) { - System.err.printf("3 Change foreColor (RGB)\n"); - } - } else if ((lCell.getForeColor() == lastAttr.getForeColor()) - && (lCell.getBackColor() != lastAttr.getBackColor()) - && (!lCell.isRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Attributes same, backColor different - sb.append(color(lCell.isBold(), - lCell.getBackColor(), false)); - - if (debugToStderr) { - System.err.printf("4 Change backColor\n"); - } - } else if (lCell.isRGB() - && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB()) - && (lCell.getBackColorRGB() != lastAttr.getBackColorRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - // Attributes same, foreColor different - sb.append(colorRGB(lCell.getBackColorRGB(), false)); - - if (debugToStderr) { - System.err.printf("4 Change backColor (RGB)\n"); - } - } else if ((lCell.getForeColor() == lastAttr.getForeColor()) - && (lCell.getBackColor() == lastAttr.getBackColor()) - && (lCell.getForeColorRGB() == lastAttr.getForeColorRGB()) - && (lCell.getBackColorRGB() == lastAttr.getBackColorRGB()) - && (lCell.isBold() == lastAttr.isBold()) - && (lCell.isReverse() == lastAttr.isReverse()) - && (lCell.isUnderline() == lastAttr.isUnderline()) - && (lCell.isBlink() == lastAttr.isBlink()) - ) { - - // All attributes the same, just print the char - // NOP - - if (debugToStderr) { - System.err.printf("5 Only emit character\n"); - } - } else { - // Just reset everything again - if (!lCell.isRGB()) { - sb.append(color(lCell.getForeColor(), - lCell.getBackColor(), - lCell.isBold(), - lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - - if (debugToStderr) { - System.err.printf("6 Change all attributes\n"); - } - } else { - sb.append(colorRGB(lCell.getForeColorRGB(), - lCell.getBackColorRGB(), - lCell.isBold(), - lCell.isReverse(), - lCell.isBlink(), - lCell.isUnderline())); - if (debugToStderr) { - System.err.printf("6 Change all attributes (RGB)\n"); - } - } - - } - // Emit the character - 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; - lastAttr.setTo(lCell); - - // Physical is always updated - physical[x][y].setTo(lCell); - - } // if (!lCell.equals(pCell) || (reallyCleared == true)) - - } // for (int x = 0; x < width; 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(final StringBuilder sb) { - CellAttributes attr = null; - - 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++) { - 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]; - - if (!lCell.isImage() - || (!wideCharImages - && (lCell.getWidth() != Cell.Width.SINGLE)) - ) { - continue; - } - - int left = x; - int right = x; - while ((right < width) - && (logical[right][y].isImage()) - && (!logical[right][y].equals(physical[right][y]) - || reallyCleared) - ) { - right++; - } - ArrayList cellsToDraw = new ArrayList(); - 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. - */ - private void resetParser() { - state = ParseState.GROUND; - params = new ArrayList(); - params.clear(); - params.add(""); - decPrivateModeFlag = false; - } - - /** - * Produce a control character or one of the special ones (ENTER, TAB, - * etc.). - * - * @param ch Unicode code point - * @param alt if true, set alt on the TKeypress - * @return one TKeypress event, either a control character (e.g. isKey == - * false, ch == 'A', ctrl == true), or a special key (e.g. isKey == true, - * fnKey == ESC) - */ - private TKeypressEvent controlChar(final char ch, final boolean alt) { - // System.err.printf("controlChar: %02x\n", ch); - - switch (ch) { - case 0x0D: - // Carriage return --> ENTER - return new TKeypressEvent(kbEnter, alt, false, false); - case 0x0A: - // Linefeed --> ENTER - return new TKeypressEvent(kbEnter, alt, false, false); - case 0x1B: - // ESC - return new TKeypressEvent(kbEsc, alt, false, false); - case '\t': - // TAB - return new TKeypressEvent(kbTab, alt, false, false); - default: - // Make all other control characters come back as the alphabetic - // character with the ctrl field set. So SOH would be 'A' + - // ctrl. - return new TKeypressEvent(false, 0, (char)(ch + 0x40), - alt, true, false); - } - } - - /** - * Produce special key from CSI Pn ; Pm ; ... ~ - * - * @return one KEYPRESS event representing a special key - */ - private TInputEvent csiFnKey() { - int key = 0; - if (params.size() > 0) { - key = Integer.parseInt(params.get(0)); - } - boolean alt = false; - boolean ctrl = false; - boolean shift = false; - if (params.size() > 1) { - shift = csiIsShift(params.get(1)); - alt = csiIsAlt(params.get(1)); - ctrl = csiIsCtrl(params.get(1)); - } - - switch (key) { - case 1: - return new TKeypressEvent(kbHome, alt, ctrl, shift); - case 2: - return new TKeypressEvent(kbIns, alt, ctrl, shift); - case 3: - return new TKeypressEvent(kbDel, alt, ctrl, shift); - case 4: - return new TKeypressEvent(kbEnd, alt, ctrl, shift); - case 5: - return new TKeypressEvent(kbPgUp, alt, ctrl, shift); - case 6: - return new TKeypressEvent(kbPgDn, alt, ctrl, shift); - case 15: - return new TKeypressEvent(kbF5, alt, ctrl, shift); - case 17: - return new TKeypressEvent(kbF6, alt, ctrl, shift); - case 18: - return new TKeypressEvent(kbF7, alt, ctrl, shift); - case 19: - return new TKeypressEvent(kbF8, alt, ctrl, shift); - case 20: - return new TKeypressEvent(kbF9, alt, ctrl, shift); - case 21: - return new TKeypressEvent(kbF10, alt, ctrl, shift); - case 23: - return new TKeypressEvent(kbF11, alt, ctrl, shift); - case 24: - return new TKeypressEvent(kbF12, alt, ctrl, shift); - default: - // Unknown - return null; - } - } - - /** - * Produce mouse events based on "Any event tracking" and UTF-8 - * coordinates. See - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking - * - * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event - */ - private TInputEvent parseMouse() { - int buttons = params.get(0).charAt(0) - 32; - int x = params.get(0).charAt(1) - 32 - 1; - int y = params.get(0).charAt(2) - 32 - 1; - - // Clamp X and Y to the physical screen coordinates. - if (x >= windowResize.getWidth()) { - x = windowResize.getWidth() - 1; - } - if (y >= windowResize.getHeight()) { - y = windowResize.getHeight() - 1; - } - - TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN; - boolean eventMouse1 = false; - boolean eventMouse2 = false; - boolean eventMouse3 = false; - boolean eventMouseWheelUp = false; - boolean eventMouseWheelDown = false; - - // System.err.printf("buttons: %04x\r\n", buttons); - - switch (buttons) { - case 0: - eventMouse1 = true; - mouse1 = true; - break; - case 1: - eventMouse2 = true; - mouse2 = true; - break; - case 2: - eventMouse3 = true; - mouse3 = true; - break; - case 3: - // Release or Move - if (!mouse1 && !mouse2 && !mouse3) { - eventType = TMouseEvent.Type.MOUSE_MOTION; - } else { - eventType = TMouseEvent.Type.MOUSE_UP; - } - if (mouse1) { - mouse1 = false; - eventMouse1 = true; - } - if (mouse2) { - mouse2 = false; - eventMouse2 = true; - } - if (mouse3) { - mouse3 = false; - eventMouse3 = true; - } - break; - - case 32: - // Dragging with mouse1 down - eventMouse1 = true; - mouse1 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 33: - // Dragging with mouse2 down - eventMouse2 = true; - mouse2 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 34: - // Dragging with mouse3 down - eventMouse3 = true; - mouse3 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 96: - // Dragging with mouse2 down after wheelUp - eventMouse2 = true; - mouse2 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 97: - // Dragging with mouse2 down after wheelDown - eventMouse2 = true; - mouse2 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 64: - eventMouseWheelUp = true; - break; - - case 65: - eventMouseWheelDown = true; - break; - - default: - // Unknown, just make it motion - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - } - return new TMouseEvent(eventType, x, y, x, y, - eventMouse1, eventMouse2, eventMouse3, - eventMouseWheelUp, eventMouseWheelDown); - } - - /** - * Produce mouse events based on "Any event tracking" and SGR - * coordinates. See - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking - * - * @param release if true, this was a release ('m') - * @return a MOUSE_MOTION, MOUSE_UP, or MOUSE_DOWN event - */ - private TInputEvent parseMouseSGR(final boolean release) { - // SGR extended coordinates - mode 1006 - if (params.size() < 3) { - // Invalid position, bail out. - return null; - } - int buttons = Integer.parseInt(params.get(0)); - int x = Integer.parseInt(params.get(1)) - 1; - int y = Integer.parseInt(params.get(2)) - 1; - - // Clamp X and Y to the physical screen coordinates. - if (x >= windowResize.getWidth()) { - x = windowResize.getWidth() - 1; - } - if (y >= windowResize.getHeight()) { - y = windowResize.getHeight() - 1; - } - - TMouseEvent.Type eventType = TMouseEvent.Type.MOUSE_DOWN; - boolean eventMouse1 = false; - boolean eventMouse2 = false; - boolean eventMouse3 = false; - boolean eventMouseWheelUp = false; - boolean eventMouseWheelDown = false; - - if (release) { - eventType = TMouseEvent.Type.MOUSE_UP; - } - - switch (buttons) { - case 0: - eventMouse1 = true; - break; - case 1: - eventMouse2 = true; - break; - case 2: - eventMouse3 = true; - break; - case 35: - // Motion only, no buttons down - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 32: - // Dragging with mouse1 down - eventMouse1 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 33: - // Dragging with mouse2 down - eventMouse2 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 34: - // Dragging with mouse3 down - eventMouse3 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 96: - // Dragging with mouse2 down after wheelUp - eventMouse2 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 97: - // Dragging with mouse2 down after wheelDown - eventMouse2 = true; - eventType = TMouseEvent.Type.MOUSE_MOTION; - break; - - case 64: - eventMouseWheelUp = true; - break; - - case 65: - eventMouseWheelDown = true; - break; - - default: - // Unknown, bail out - return null; - } - return new TMouseEvent(eventType, x, y, x, y, - eventMouse1, eventMouse2, eventMouse3, - eventMouseWheelUp, eventMouseWheelDown); - } - - /** - * Return any events in the IO queue due to timeout. - * - * @param queue list to append new events to - */ - private void getIdleEvents(final List queue) { - long nowTime = System.currentTimeMillis(); - - // 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(); - - if ((newWidth != windowResize.getWidth()) - || (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, - newWidth, newHeight); - queue.add(event); - } - windowSizeTime = nowTime; - } - - // ESCDELAY type timeout - if (state == ParseState.ESCAPE) { - long escDelay = nowTime - escapeTime; - if (escDelay > 100) { - // After 0.1 seconds, assume a true escape character - queue.add(controlChar((char)0x1B, false)); - resetParser(); - } - } - } - - /** - * Returns true if the CSI parameter for a keyboard command means that - * shift was down. - */ - private boolean csiIsShift(final String x) { - if ((x.equals("2")) - || (x.equals("4")) - || (x.equals("6")) - || (x.equals("8")) - ) { - return true; - } - return false; - } - - /** - * Returns true if the CSI parameter for a keyboard command means that - * alt was down. - */ - private boolean csiIsAlt(final String x) { - if ((x.equals("3")) - || (x.equals("4")) - || (x.equals("7")) - || (x.equals("8")) - ) { - return true; - } - return false; - } - - /** - * Returns true if the CSI parameter for a keyboard command means that - * ctrl was down. - */ - private boolean csiIsCtrl(final String x) { - if ((x.equals("5")) - || (x.equals("6")) - || (x.equals("7")) - || (x.equals("8")) - ) { - return true; - } - return false; - } - - /** - * Parses the next character of input to see if an InputEvent is - * fully here. - * - * @param events list to append new events to - * @param ch Unicode code point - */ - private void processChar(final List events, final char ch) { - - // ESCDELAY type timeout - long nowTime = System.currentTimeMillis(); - if (state == ParseState.ESCAPE) { - long escDelay = nowTime - escapeTime; - if (escDelay > 250) { - // After 0.25 seconds, assume a true escape character - events.add(controlChar((char)0x1B, false)); - resetParser(); - } - } - - // TKeypress fields - boolean ctrl = false; - boolean alt = false; - boolean shift = false; - - // System.err.printf("state: %s ch %c\r\n", state, ch); - - switch (state) { - case GROUND: - - if (ch == 0x1B) { - state = ParseState.ESCAPE; - escapeTime = nowTime; - return; - } - - if (ch <= 0x1F) { - // Control character - events.add(controlChar(ch, false)); - resetParser(); - return; - } - - if (ch >= 0x20) { - // Normal character - events.add(new TKeypressEvent(false, 0, ch, - false, false, false)); - resetParser(); - return; - } - - break; - - case ESCAPE: - if (ch <= 0x1F) { - // ALT-Control character - events.add(controlChar(ch, true)); - resetParser(); - return; - } - - if (ch == 'O') { - // This will be one of the function keys - state = ParseState.ESCAPE_INTERMEDIATE; - return; - } - - // '[' goes to CSI_ENTRY - if (ch == '[') { - state = ParseState.CSI_ENTRY; - return; - } - - // Everything else is assumed to be Alt-keystroke - if ((ch >= 'A') && (ch <= 'Z')) { - shift = true; - } - alt = true; - events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift)); - resetParser(); - return; - - case ESCAPE_INTERMEDIATE: - if ((ch >= 'P') && (ch <= 'S')) { - // Function key - switch (ch) { - case 'P': - events.add(new TKeypressEvent(kbF1)); - break; - case 'Q': - events.add(new TKeypressEvent(kbF2)); - break; - case 'R': - events.add(new TKeypressEvent(kbF3)); - break; - case 'S': - events.add(new TKeypressEvent(kbF4)); - break; - default: - break; - } - resetParser(); - return; - } - - // Unknown keystroke, ignore - resetParser(); - return; - - case CSI_ENTRY: - // Numbers - parameter values - if ((ch >= '0') && (ch <= '9')) { - params.set(params.size() - 1, - params.get(params.size() - 1) + ch); - state = ParseState.CSI_PARAM; - return; - } - // Parameter separator - if (ch == ';') { - params.add(""); - return; - } - - if ((ch >= 0x30) && (ch <= 0x7E)) { - switch (ch) { - case 'A': - // Up - events.add(new TKeypressEvent(kbUp, alt, ctrl, shift)); - resetParser(); - return; - case 'B': - // Down - events.add(new TKeypressEvent(kbDown, alt, ctrl, shift)); - resetParser(); - return; - case 'C': - // Right - events.add(new TKeypressEvent(kbRight, alt, ctrl, shift)); - resetParser(); - return; - case 'D': - // Left - events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift)); - resetParser(); - return; - case 'H': - // Home - events.add(new TKeypressEvent(kbHome)); - resetParser(); - return; - case 'F': - // End - events.add(new TKeypressEvent(kbEnd)); - resetParser(); - return; - case 'Z': - // CBT - Cursor backward X tab stops (default 1) - events.add(new TKeypressEvent(kbBackTab)); - resetParser(); - return; - case 'M': - // Mouse position - state = ParseState.MOUSE; - return; - case '<': - // Mouse position, SGR (1006) coordinates - state = ParseState.MOUSE_SGR; - return; - case '?': - // DEC private mode flag - decPrivateModeFlag = true; - return; - default: - break; - } - } - - // Unknown keystroke, ignore - resetParser(); - return; - - case MOUSE_SGR: - // Numbers - parameter values - if ((ch >= '0') && (ch <= '9')) { - params.set(params.size() - 1, - params.get(params.size() - 1) + ch); - return; - } - // Parameter separator - if (ch == ';') { - params.add(""); - return; - } - - switch (ch) { - case 'M': - // Generate a mouse press event - TInputEvent event = parseMouseSGR(false); - if (event != null) { - events.add(event); - } - resetParser(); - return; - case 'm': - // Generate a mouse release event - event = parseMouseSGR(true); - if (event != null) { - events.add(event); - } - resetParser(); - return; - default: - break; - } - - // Unknown keystroke, ignore - resetParser(); - return; - - case CSI_PARAM: - // Numbers - parameter values - if ((ch >= '0') && (ch <= '9')) { - params.set(params.size() - 1, - params.get(params.size() - 1) + ch); - state = ParseState.CSI_PARAM; - return; - } - // Parameter separator - if (ch == ';') { - params.add(""); - return; - } - - if (ch == '~') { - events.add(csiFnKey()); - resetParser(); - return; - } - - if ((ch >= 0x30) && (ch <= 0x7E)) { - switch (ch) { - case 'A': - // Up - if (params.size() > 1) { - shift = csiIsShift(params.get(1)); - alt = csiIsAlt(params.get(1)); - ctrl = csiIsCtrl(params.get(1)); - } - events.add(new TKeypressEvent(kbUp, alt, ctrl, shift)); - resetParser(); - return; - case 'B': - // Down - if (params.size() > 1) { - shift = csiIsShift(params.get(1)); - alt = csiIsAlt(params.get(1)); - ctrl = csiIsCtrl(params.get(1)); - } - events.add(new TKeypressEvent(kbDown, alt, ctrl, shift)); - resetParser(); - return; - case 'C': - // Right - if (params.size() > 1) { - shift = csiIsShift(params.get(1)); - alt = csiIsAlt(params.get(1)); - ctrl = csiIsCtrl(params.get(1)); - } - events.add(new TKeypressEvent(kbRight, alt, ctrl, shift)); - resetParser(); - return; - case 'D': - // Left - if (params.size() > 1) { - shift = csiIsShift(params.get(1)); - alt = csiIsAlt(params.get(1)); - ctrl = csiIsCtrl(params.get(1)); - } - events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift)); - resetParser(); - return; - case 'H': - // Home - if (params.size() > 1) { - shift = csiIsShift(params.get(1)); - alt = csiIsAlt(params.get(1)); - ctrl = csiIsCtrl(params.get(1)); - } - events.add(new TKeypressEvent(kbHome, alt, ctrl, shift)); - resetParser(); - return; - case 'F': - // End - if (params.size() > 1) { - shift = csiIsShift(params.get(1)); - alt = csiIsAlt(params.get(1)); - ctrl = csiIsCtrl(params.get(1)); - } - 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; - } - } - - // Unknown keystroke, ignore - resetParser(); - return; - - case MOUSE: - params.set(0, params.get(params.size() - 1) + ch); - if (params.get(0).length() == 3) { - // We have enough to generate a mouse event - events.add(parseMouse()); - resetParser(); - } - return; - - default: - break; - } - - // This "should" be impossible to reach - 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 - * enabled. - * - * @param on if true, enable metaSendsEscape - * @return the string to emit to xterm - */ - private String xtermMetaSendsEscape(final boolean on) { - if (on) { - return "\033[?1036h\033[?1034l"; - } - return "\033[?1036l"; - } - - /** - * Create an xterm OSC sequence to change the window title. - * - * @param title the new title - * @return the string to emit to xterm - */ - private String getSetTitleString(final String title) { - 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 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 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 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. - * - * @param bold if true, set bold - * @param color one of the Color.WHITE, Color.BLUE, etc. constants - * @param foreground if true, this is a foreground color - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[42m" - */ - private String color(final boolean bold, final Color color, - final boolean foreground) { - return color(color, foreground, true) + - rgbColor(bold, color, foreground); - } - - /** - * Create a T.416 RGB parameter sequence for a single color change. - * - * @param colorRGB a 24-bit RGB value for foreground color - * @param foreground if true, this is a foreground color - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[42m" - */ - private String colorRGB(final int colorRGB, final boolean foreground) { - - int colorRed = (colorRGB >>> 16) & 0xFF; - int colorGreen = (colorRGB >>> 8) & 0xFF; - int colorBlue = colorRGB & 0xFF; - - StringBuilder sb = new StringBuilder(); - if (foreground) { - sb.append("\033[38;2;"); - } else { - sb.append("\033[48;2;"); - } - sb.append(String.format("%d;%d;%dm", colorRed, colorGreen, colorBlue)); - return sb.toString(); - } - - /** - * Create a T.416 RGB parameter sequence for both foreground and - * background color change. - * - * @param foreColorRGB a 24-bit RGB value for foreground color - * @param backColorRGB a 24-bit RGB value for foreground color - * @return the string to emit to an ANSI / ECMA-style terminal, - * 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; - - StringBuilder sb = new StringBuilder(); - sb.append(String.format("\033[38;2;%d;%d;%dm", - foreColorRed, foreColorGreen, foreColorBlue)); - sb.append(String.format("\033[48;2;%d;%d;%dm", - backColorRed, backColorGreen, backColorBlue)); - return sb.toString(); - } - - /** - * Create a T.416 RGB parameter sequence for a single color change. - * - * @param bold if true, set bold - * @param color one of the Color.WHITE, Color.BLUE, etc. constants - * @param foreground if true, this is a foreground color - * @return the string to emit to an xterm terminal with RGB support, - * e.g. "\033[38;2;RR;GG;BBm" - */ - private String rgbColor(final boolean bold, final Color color, - final boolean foreground) { - if (doRgbColor == false) { - return ""; - } - StringBuilder sb = new StringBuilder("\033["); - if (bold) { - // Bold implies foreground only - sb.append("38;2;"); - if (color.equals(Color.BLACK)) { - sb.append(systemColorRGB(MYBOLD_BLACK)); - } else if (color.equals(Color.RED)) { - sb.append(systemColorRGB(MYBOLD_RED)); - } else if (color.equals(Color.GREEN)) { - sb.append(systemColorRGB(MYBOLD_GREEN)); - } else if (color.equals(Color.YELLOW)) { - sb.append(systemColorRGB(MYBOLD_YELLOW)); - } else if (color.equals(Color.BLUE)) { - sb.append(systemColorRGB(MYBOLD_BLUE)); - } else if (color.equals(Color.MAGENTA)) { - sb.append(systemColorRGB(MYBOLD_MAGENTA)); - } else if (color.equals(Color.CYAN)) { - sb.append(systemColorRGB(MYBOLD_CYAN)); - } else if (color.equals(Color.WHITE)) { - sb.append(systemColorRGB(MYBOLD_WHITE)); - } - } else { - if (foreground) { - sb.append("38;2;"); - } else { - sb.append("48;2;"); - } - if (color.equals(Color.BLACK)) { - sb.append(systemColorRGB(MYBLACK)); - } else if (color.equals(Color.RED)) { - sb.append(systemColorRGB(MYRED)); - } else if (color.equals(Color.GREEN)) { - sb.append(systemColorRGB(MYGREEN)); - } else if (color.equals(Color.YELLOW)) { - sb.append(systemColorRGB(MYYELLOW)); - } else if (color.equals(Color.BLUE)) { - sb.append(systemColorRGB(MYBLUE)); - } else if (color.equals(Color.MAGENTA)) { - sb.append(systemColorRGB(MYMAGENTA)); - } else if (color.equals(Color.CYAN)) { - sb.append(systemColorRGB(MYCYAN)); - } else if (color.equals(Color.WHITE)) { - sb.append(systemColorRGB(MYWHITE)); - } - } - sb.append("m"); - return sb.toString(); - } - - /** - * Create a T.416 RGB parameter sequence for both foreground and - * background color change. - * - * @param bold if true, set bold - * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants - * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants - * @return the string to emit to an xterm terminal with RGB support, - * e.g. "\033[38;2;RR;GG;BB;48;2;RR;GG;BBm" - */ - private String rgbColor(final boolean bold, final Color foreColor, - final Color backColor) { - if (doRgbColor == false) { - return ""; - } - - return rgbColor(bold, foreColor, true) + - rgbColor(false, backColor, false); - } - - /** - * Create a SGR parameter sequence for a single color change. - * - * @param color one of the Color.WHITE, Color.BLUE, etc. constants - * @param foreground if true, this is a foreground color - * @param header if true, make the full header, otherwise just emit the - * color parameter e.g. "42;" - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[42m" - */ - private String color(final Color color, final boolean foreground, - final boolean header) { - - int ecmaColor = color.getValue(); - - // Convert Color.* values to SGR numerics - if (foreground) { - ecmaColor += 30; - } else { - ecmaColor += 40; - } - - if (header) { - return String.format("\033[%dm", ecmaColor); - } else { - return String.format("%d;", ecmaColor); - } - } - - /** - * Create a SGR parameter sequence for both foreground and background - * color change. - * - * @param bold if true, set bold - * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants - * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[31;42m" - */ - private String color(final boolean bold, final Color foreColor, - final Color backColor) { - return color(foreColor, backColor, true) + - rgbColor(bold, foreColor, backColor); - } - - /** - * Create a SGR parameter sequence for both foreground and - * background color change. - * - * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants - * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants - * @param header if true, make the full header, otherwise just emit the - * color parameter e.g. "31;42;" - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[31;42m" - */ - private String color(final Color foreColor, final Color backColor, - final boolean header) { - - int ecmaForeColor = foreColor.getValue(); - int ecmaBackColor = backColor.getValue(); - - // Convert Color.* values to SGR numerics - ecmaBackColor += 40; - ecmaForeColor += 30; - - if (header) { - return String.format("\033[%d;%dm", ecmaForeColor, ecmaBackColor); - } else { - return String.format("%d;%d;", ecmaForeColor, ecmaBackColor); - } - } - - /** - * Create a SGR parameter sequence for foreground, background, and - * several attributes. This sequence first resets all attributes to - * default, then sets attributes as per the parameters. - * - * @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants - * @param backColor one of the Color.WHITE, Color.BLUE, etc. constants - * @param bold if true, set bold - * @param reverse if true, set reverse - * @param blink if true, set blink - * @param underline if true, set underline - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[0;1;31;42m" - */ - private String color(final Color foreColor, final Color backColor, - final boolean bold, final boolean reverse, final boolean blink, - final boolean underline) { - - int ecmaForeColor = foreColor.getValue(); - int ecmaBackColor = backColor.getValue(); - - // Convert Color.* values to SGR numerics - ecmaBackColor += 40; - ecmaForeColor += 30; - - StringBuilder sb = new StringBuilder(); - if ( bold && reverse && blink && !underline ) { - sb.append("\033[0;1;7;5;"); - } else if ( bold && reverse && !blink && !underline ) { - sb.append("\033[0;1;7;"); - } else if ( !bold && reverse && blink && !underline ) { - sb.append("\033[0;7;5;"); - } else if ( bold && !reverse && blink && !underline ) { - sb.append("\033[0;1;5;"); - } else if ( bold && !reverse && !blink && !underline ) { - sb.append("\033[0;1;"); - } else if ( !bold && reverse && !blink && !underline ) { - sb.append("\033[0;7;"); - } else if ( !bold && !reverse && blink && !underline) { - sb.append("\033[0;5;"); - } else if ( bold && reverse && blink && underline ) { - sb.append("\033[0;1;7;5;4;"); - } else if ( bold && reverse && !blink && underline ) { - sb.append("\033[0;1;7;4;"); - } else if ( !bold && reverse && blink && underline ) { - sb.append("\033[0;7;5;4;"); - } else if ( bold && !reverse && blink && underline ) { - sb.append("\033[0;1;5;4;"); - } else if ( bold && !reverse && !blink && underline ) { - sb.append("\033[0;1;4;"); - } else if ( !bold && reverse && !blink && underline ) { - sb.append("\033[0;7;4;"); - } else if ( !bold && !reverse && blink && underline) { - sb.append("\033[0;5;4;"); - } else if ( !bold && !reverse && !blink && underline) { - sb.append("\033[0;4;"); - } else { - assert (!bold && !reverse && !blink && !underline); - sb.append("\033[0;"); - } - sb.append(String.format("%d;%dm", ecmaForeColor, ecmaBackColor)); - sb.append(rgbColor(bold, foreColor, backColor)); - return sb.toString(); - } - - /** - * Create a SGR parameter sequence for foreground, background, and - * several attributes. This sequence first resets all attributes to - * default, then sets attributes as per the parameters. - * - * @param foreColorRGB a 24-bit RGB value for foreground color - * @param backColorRGB a 24-bit RGB value for foreground color - * @param bold if true, set bold - * @param reverse if true, set reverse - * @param blink if true, set blink - * @param underline if true, set underline - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[0;1;31;42m" - */ - private String colorRGB(final int foreColorRGB, final int backColorRGB, - 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; - - StringBuilder sb = new StringBuilder(); - if ( bold && reverse && blink && !underline ) { - sb.append("\033[0;1;7;5;"); - } else if ( bold && reverse && !blink && !underline ) { - sb.append("\033[0;1;7;"); - } else if ( !bold && reverse && blink && !underline ) { - sb.append("\033[0;7;5;"); - } else if ( bold && !reverse && blink && !underline ) { - sb.append("\033[0;1;5;"); - } else if ( bold && !reverse && !blink && !underline ) { - sb.append("\033[0;1;"); - } else if ( !bold && reverse && !blink && !underline ) { - sb.append("\033[0;7;"); - } else if ( !bold && !reverse && blink && !underline) { - sb.append("\033[0;5;"); - } else if ( bold && reverse && blink && underline ) { - sb.append("\033[0;1;7;5;4;"); - } else if ( bold && reverse && !blink && underline ) { - sb.append("\033[0;1;7;4;"); - } else if ( !bold && reverse && blink && underline ) { - sb.append("\033[0;7;5;4;"); - } else if ( bold && !reverse && blink && underline ) { - sb.append("\033[0;1;5;4;"); - } else if ( bold && !reverse && !blink && underline ) { - sb.append("\033[0;1;4;"); - } else if ( !bold && reverse && !blink && underline ) { - sb.append("\033[0;7;4;"); - } else if ( !bold && !reverse && blink && underline) { - sb.append("\033[0;5;4;"); - } else if ( !bold && !reverse && !blink && underline) { - sb.append("\033[0;4;"); - } else { - assert (!bold && !reverse && !blink && !underline); - sb.append("\033[0;"); - } - - sb.append("m\033[38;2;"); - sb.append(String.format("%d;%d;%d", foreColorRed, foreColorGreen, - foreColorBlue)); - sb.append("m\033[48;2;"); - sb.append(String.format("%d;%d;%d", backColorRed, backColorGreen, - backColorBlue)); - sb.append("m"); - return sb.toString(); - } - - /** - * 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" - */ - private String normal() { - 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. - * - * @param header if true, make the full header, otherwise just emit the - * bare parameter e.g. "0;" - * @return the string to emit to an ANSI / ECMA-style terminal, - * e.g. "\033[0m" - */ - private String normal(final boolean header) { - if (header) { - return "\033[0;37;40m"; - } - return "0;37;40"; - } - - /** - * Create a SGR parameter sequence for enabling the visible cursor. - * - * @param on if true, turn on cursor - * @return the string to emit to an ANSI / ECMA-style terminal - */ - private String cursor(final boolean on) { - if (on && !cursorOn) { - cursorOn = true; - return "\033[?25h"; - } - if (!on && cursorOn) { - cursorOn = false; - return "\033[?25l"; - } - return ""; - } - - /** - * Clear the entire screen. Because some terminals use back-color-erase, - * set the color to white-on-black beforehand. - * - * @return the string to emit to an ANSI / ECMA-style terminal - */ - private String clearAll() { - return "\033[0;37;40m\033[2J"; - } - - /** - * Clear the line from the cursor (inclusive) to the end of the screen. - * Because some terminals use back-color-erase, set the color to - * white-on-black beforehand. - * - * @return the string to emit to an ANSI / ECMA-style terminal - */ - private String clearRemainingLine() { - return "\033[0;37;40m\033[K"; - } - - /** - * Move the cursor to (x, y). - * - * @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 gotoXY(final int x, final int y) { - return String.format("\033[%d;%dH", y + 1, x + 1); - } - - /** - * Tell (u)xterm that we want to receive mouse events based on "Any event - * tracking", UTF-8 coordinates, and then SGR coordinates. Ideally we - * will end up with SGR coordinates with UTF-8 coordinates as a fallback. - * See - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking - * - * 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. - * @return the string to emit to xterm - */ - private String mouse(final boolean on) { - if (on) { - return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\"; - } - return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\"; - } - -}