X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fbackend%2FECMA48Terminal.java;h=429e698d733177cd59f27095088afd71b4b4ec1f;hb=505be508ae7d3fb48122be548b310a238cfb91eb;hp=39ca236552d37302786fa1be4747d50b425aa7ec;hpb=e6469faa3f6895ec0ff9b7592a7348a321898b71;p=fanfix.git diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index 39ca236..429e698 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -28,8 +28,12 @@ */ package jexer.backend; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; 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; @@ -44,11 +48,12 @@ 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.bits.StringUtils; import jexer.event.TCommandEvent; import jexer.event.TInputEvent; import jexer.event.TKeypressEvent; @@ -82,12 +87,14 @@ public class ECMA48Terminal extends LogicalScreen } /** - * Number of colors in the sixel palette. Xterm 335 defines the max as - * 1024. + * Available Jexer images support. */ - private static final int MAX_COLOR_REGISTERS = 1024; - // Black-and-white is possible too. - // private static final int MAX_COLOR_REGISTERS = 2; + private enum JexerImageOption { + DISABLED, + JPG, + PNG, + RGB, + } // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- @@ -175,6 +182,11 @@ public class ECMA48Terminal extends LogicalScreen */ private TResizeEvent windowResize = null; + /** + * If true, emit wide-char (CJK/Emoji) characters as sixel images. + */ + private boolean wideCharImages = true; + /** * Window width in pixels. Used for sixel support. */ @@ -190,6 +202,11 @@ public class ECMA48Terminal extends LogicalScreen */ private boolean sixel = true; + /** + * If true, use a single shared palette for sixel. + */ + private boolean sixelSharedPalette = true; + /** * The sixel palette handler. */ @@ -198,12 +215,45 @@ public class ECMA48Terminal extends LogicalScreen /** * The sixel post-rendered string cache. */ - private SixelCache sixelCache = null; + 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 not DISABLED, emit image data via Jexer image protocol if the + * terminal supports it. + */ + private JexerImageOption jexerImageOption = JexerImageOption.JPG; + + /** + * The Jexer post-rendered string cache. + */ + private ImageCache jexerCache = null; /** * If true, then we changed System.in and need to change it back. */ - private boolean setRawMode; + private boolean setRawMode = false; + + /** + * If true, '?' was seen in terminal response. + */ + private boolean decPrivateModeFlag = false; /** * The terminal's input. If an InputStream is not specified in the @@ -232,9 +282,27 @@ public class ECMA48Terminal extends LogicalScreen */ private Object listener; + // Colors to map DOS colors to AWT colors. + private static java.awt.Color MYBLACK; + private static java.awt.Color MYRED; + private static java.awt.Color MYGREEN; + private static java.awt.Color MYYELLOW; + private static java.awt.Color MYBLUE; + private static java.awt.Color MYMAGENTA; + private static java.awt.Color MYCYAN; + private static java.awt.Color MYWHITE; + private static java.awt.Color MYBOLD_BLACK; + private static java.awt.Color MYBOLD_RED; + private static java.awt.Color MYBOLD_GREEN; + private static java.awt.Color MYBOLD_YELLOW; + private static java.awt.Color MYBOLD_BLUE; + private static java.awt.Color MYBOLD_MAGENTA; + private static java.awt.Color MYBOLD_CYAN; + private static java.awt.Color MYBOLD_WHITE; + /** * SixelPalette is used to manage the conversion of images between 24-bit - * RGB color and a palette of MAX_COLOR_REGISTERS colors. + * RGB color and a palette of sixelPaletteSize colors. */ private class SixelPalette { @@ -247,7 +315,7 @@ public class ECMA48Terminal extends LogicalScreen * Map of color palette index for sixel output, from the order it was * generated by makePalette() to rgbColors. */ - private int [] rgbSortedIndex = new int[MAX_COLOR_REGISTERS]; + private int [] rgbSortedIndex = new int[sixelPaletteSize]; /** * The color palette, organized by hue, saturation, and luminance. @@ -345,7 +413,7 @@ public class ECMA48Terminal extends LogicalScreen int green = (color >>> 8) & 0xFF; int blue = color & 0xFF; - if (MAX_COLOR_REGISTERS == 2) { + if (sixelPaletteSize == 2) { if (((red * red) + (green * green) + (blue * blue)) < 35568) { // Black return 0; @@ -427,7 +495,7 @@ public class ECMA48Terminal extends LogicalScreen ((255 - blue) * (255 - blue))) < diff) { // White is a closer match. - idx = MAX_COLOR_REGISTERS - 1; + idx = sixelPaletteSize - 1; } assert (idx != -1); return idx; @@ -450,7 +518,7 @@ public class ECMA48Terminal extends LogicalScreen } /** - * Dither an image to a MAX_COLOR_REGISTERS palette. The dithered + * Dither an image to a sixelPaletteSize palette. The dithered * image cells will contain indexes into the palette. * * @param image the image to dither @@ -473,7 +541,7 @@ public class ECMA48Terminal extends LogicalScreen imageY) & 0xFFFFFF; int colorIdx = matchColor(oldPixel); assert (colorIdx >= 0); - assert (colorIdx < MAX_COLOR_REGISTERS); + assert (colorIdx < sixelPaletteSize); int newPixel = rgbColors.get(colorIdx); ditheredImage.setRGB(imageX, imageY, colorIdx); @@ -671,11 +739,11 @@ public class ECMA48Terminal extends LogicalScreen private void makePalette() { // Generate the sixel palette. Because we have no idea at this // layer which image(s) will be shown, we have to use a common - // palette with MAX_COLOR_REGISTERS colors for everything, and + // palette with sixelPaletteSize colors for everything, and // map the BufferedImage colors to their nearest neighbor in RGB // space. - if (MAX_COLOR_REGISTERS == 2) { + if (sixelPaletteSize == 2) { rgbColors.add(0); rgbColors.add(0xFFFFFF); rgbSortedIndex[0] = 0; @@ -696,13 +764,13 @@ public class ECMA48Terminal extends LogicalScreen satBits = 2; lumBits = 1; - assert (MAX_COLOR_REGISTERS >= 256); - assert ((MAX_COLOR_REGISTERS == 256) - || (MAX_COLOR_REGISTERS == 512) - || (MAX_COLOR_REGISTERS == 1024) - || (MAX_COLOR_REGISTERS == 2048)); + assert (sixelPaletteSize >= 256); + assert ((sixelPaletteSize == 256) + || (sixelPaletteSize == 512) + || (sixelPaletteSize == 1024) + || (sixelPaletteSize == 2048)); - switch (MAX_COLOR_REGISTERS) { + switch (sixelPaletteSize) { case 512: hueBits = 5; satBits = 2; @@ -788,7 +856,7 @@ public class ECMA48Terminal extends LogicalScreen } // System.err.printf("\n\n"); - assert (rgbColors.size() == MAX_COLOR_REGISTERS); + assert (rgbColors.size() == sixelPaletteSize); /* * We need to sort rgbColors, so that toSixel() can know where @@ -800,19 +868,19 @@ public class ECMA48Terminal extends LogicalScreen Collections.sort(rgbColors); HashMap rgbColorIndices = null; rgbColorIndices = new HashMap(); - for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + for (int i = 0; i < sixelPaletteSize; i++) { rgbColorIndices.put(rgbColors.get(i), i); } - for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + for (int i = 0; i < sixelPaletteSize; i++) { int rawColor = rawRgbList.get(i); rgbSortedIndex[i] = rgbColorIndices.get(rawColor); } if (DEBUG) { - for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + for (int i = 0; i < sixelPaletteSize; i++) { assert (rawRgbList != null); int idx = rgbSortedIndex[i]; int rgbColor = rgbColors.get(idx); - if ((idx != 0) && (idx != MAX_COLOR_REGISTERS - 1)) { + if ((idx != 0) && (idx != sixelPaletteSize - 1)) { /* System.err.printf("%d %06x --> %d %06x\n", i, rawRgbList.get(i), idx, rgbColors.get(idx)); @@ -825,7 +893,7 @@ public class ECMA48Terminal extends LogicalScreen // Set the dimmest color as true black, and the brightest as true // white. rgbColors.set(0, 0); - rgbColors.set(MAX_COLOR_REGISTERS - 1, 0xFFFFFF); + rgbColors.set(sixelPaletteSize - 1, 0xFFFFFF); /* System.err.printf("\n"); @@ -850,7 +918,7 @@ public class ECMA48Terminal extends LogicalScreen public String emitPalette(final StringBuilder sb, final boolean [] used) { - for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + for (int i = 0; i < sixelPaletteSize; i++) { if (((used != null) && (used[i] == true)) || (used == null)) { int rgbColor = rgbColors.get(i); sb.append(String.format("#%d;2;%d;%d;%d", i, @@ -864,10 +932,10 @@ public class ECMA48Terminal extends LogicalScreen } /** - * SixelCache is a least-recently-used cache that hangs on to the - * post-rendered sixel string for a particular set of cells. + * 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 SixelCache { + private class ImageCache { /** * Maximum size of the cache. @@ -916,7 +984,7 @@ public class ECMA48Terminal extends LogicalScreen * * @param maxSize the maximum size of the cache */ - public SixelCache(final int maxSize) { + public ImageCache(final int maxSize) { this.maxSize = maxSize; cache = new HashMap(); } @@ -1002,14 +1070,16 @@ public class ECMA48Terminal extends LogicalScreen // ------------------------------------------------------------------------ /** - * Constructor sets up state for getEvent(). + * Constructor sets up state for getEvent(). If either windowWidth or + * windowHeight are less than 1, the terminal is not resized. * * @param listener the object this backend needs to wake up when new * input comes in * @param input an InputStream connected to the remote user, or null for * System.in. If System.in is used, then on non-Windows systems it will - * be put in raw mode; shutdown() will (blindly!) put System.in in cooked - * mode. input is always converted to a Reader with UTF-8 encoding. + * be put in raw mode; closeTerminal() will (blindly!) put System.in in + * cooked mode. input is always converted to a Reader with UTF-8 + * encoding. * @param output an OutputStream connected to the remote user, or null * for System.out. output is always converted to a Writer with UTF-8 * encoding. @@ -1026,10 +1096,12 @@ public class ECMA48Terminal extends LogicalScreen // Send dtterm/xterm sequences, which will probably not work because // allowWindowOps is defaulted to false. - String resizeString = String.format("\033[8;%d;%dt", windowHeight, - windowWidth); - this.output.write(resizeString); - this.output.flush(); + if ((windowWidth > 0) && (windowHeight > 0)) { + String resizeString = String.format("\033[8;%d;%dt", windowHeight, + windowWidth); + this.output.write(resizeString); + this.output.flush(); + } } /** @@ -1039,8 +1111,9 @@ public class ECMA48Terminal extends LogicalScreen * input comes in * @param input an InputStream connected to the remote user, or null for * System.in. If System.in is used, then on non-Windows systems it will - * be put in raw mode; shutdown() will (blindly!) put System.in in cooked - * mode. input is always converted to a Reader with UTF-8 encoding. + * be put in raw mode; closeTerminal() will (blindly!) put System.in in + * cooked mode. input is always converted to a Reader with UTF-8 + * encoding. * @param output an OutputStream connected to the remote user, or null * for System.out. output is always converted to a Writer with UTF-8 * encoding. @@ -1089,11 +1162,18 @@ public class ECMA48Terminal extends LogicalScreen "UTF-8")); } - // Request xterm report window dimensions in pixels - this.output.printf("%s", xtermReportWindowPixelDimensions()); + // 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)); + + // Request xterm use the sixel settings we want + this.output.printf("%s", xtermSetSixelSettings()); + this.output.flush(); // Query the screen size @@ -1175,11 +1255,18 @@ public class ECMA48Terminal extends LogicalScreen this.output = writer; - // Request xterm report window dimensions in pixels - this.output.printf("%s", xtermReportWindowPixelDimensions()); + // 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)); + + // Request xterm use the sixel settings we want + this.output.printf("%s", xtermSetSixelSettings()); + this.output.flush(); // Query the screen size @@ -1307,7 +1394,7 @@ public class ECMA48Terminal extends LogicalScreen */ public void closeTerminal() { - // System.err.println("=== shutdown() ==="); System.err.flush(); + // System.err.println("=== closeTerminal() ==="); System.err.flush(); // Tell the reader thread to stop looking at input stopReaderThread = true; @@ -1322,7 +1409,8 @@ public class ECMA48Terminal extends LogicalScreen // Disable mouse reporting and show cursor. Defensive null check // here in case closeTerminal() is called twice. if (output != null) { - output.printf("%s%s%s", mouse(false), cursor(true), normal()); + output.printf("%s%s%s%s", mouse(false), cursor(true), + defaultColor(), xtermResetSixelSettings()); output.flush(); } @@ -1371,12 +1459,73 @@ public class ECMA48Terminal extends LogicalScreen 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 + } + + // Shared palette + if (System.getProperty("jexer.ECMA48.sixelSharedPalette", + "true").equals("false")) { + sixelSharedPalette = false; + } else { + sixelSharedPalette = true; + } + + // Default to not supporting iTerm2 images. + if (System.getProperty("jexer.ECMA48.iTerm2Images", + "false").equals("true")) { + iterm2Images = true; + } else { + iterm2Images = false; + } + + // Default to using JPG Jexer images if terminal supports it. + String jexerImageStr = System.getProperty("jexer.ECMA48.jexerImages", + "jpg").toLowerCase(); + if (jexerImageStr.equals("false")) { + jexerImageOption = JexerImageOption.DISABLED; + } else if (jexerImageStr.equals("jpg")) { + jexerImageOption = JexerImageOption.JPG; + } else if (jexerImageStr.equals("png")) { + jexerImageOption = JexerImageOption.PNG; + } else if (jexerImageStr.equals("rgb")) { + jexerImageOption = JexerImageOption.RGB; + } + + // Set custom colors + setCustomSystemColors(); } // ------------------------------------------------------------------------ @@ -1496,7 +1645,10 @@ public class ECMA48Terminal extends LogicalScreen * @return the width in pixels of a character cell */ public int getTextWidth() { - return (widthPixels / sessionInfo.getWindowWidth()); + if (sessionInfo.getWindowWidth() > 0) { + return (widthPixels / sessionInfo.getWindowWidth()); + } + return 16; } /** @@ -1505,7 +1657,10 @@ public class ECMA48Terminal extends LogicalScreen * @return the height in pixels of a character cell */ public int getTextHeight() { - return (heightPixels / sessionInfo.getWindowHeight()); + if (sessionInfo.getWindowHeight() > 0) { + return (heightPixels / sessionInfo.getWindowHeight()); + } + return 20; } /** @@ -1671,7 +1826,11 @@ public class ECMA48Terminal extends LogicalScreen // Image cell: bypass the rest of the loop, it is not // rendered here. - if (lCell.isImage()) { + if ((wideCharImages && lCell.isImage()) + || (!wideCharImages + && lCell.isImage() + && (lCell.getWidth() == Cell.Width.SINGLE)) + ) { hasImage = true; // Save the last rendered cell @@ -1682,7 +1841,16 @@ public class ECMA48Terminal extends LogicalScreen continue; } - assert (!lCell.isImage()); + assert ((wideCharImages && !lCell.isImage()) + || (!wideCharImages + && (!lCell.isImage() + || (lCell.isImage() + && (lCell.getWidth() != Cell.Width.SINGLE))))); + + if (!wideCharImages && (lCell.getWidth() == Cell.Width.RIGHT)) { + continue; + } + if (hasImage) { hasImage = false; sb.append(gotoXY(x, y)); @@ -1842,7 +2010,13 @@ public class ECMA48Terminal extends LogicalScreen } // Emit the character - sb.append(lCell.getChar()); + if (wideCharImages + // Don't emit the right-half of full-width chars. + || (!wideCharImages + && (lCell.getWidth() != Cell.Width.RIGHT)) + ) { + sb.append(Character.toChars(lCell.getChar())); + } // Save the last rendered cell lastX = x; @@ -1873,7 +2047,7 @@ public class ECMA48Terminal extends LogicalScreen } /* - * For sixel support, draw all of the sixel output first, and then + * 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. */ @@ -1894,7 +2068,10 @@ public class ECMA48Terminal extends LogicalScreen Cell lCell = logical[x][y]; Cell pCell = physical[x][y]; - if (!lCell.isImage()) { + if (!lCell.isImage() + || (!wideCharImages + && (lCell.getWidth() != Cell.Width.SINGLE)) + ) { continue; } @@ -1916,7 +2093,13 @@ public class ECMA48Terminal extends LogicalScreen physical[x + i][y].setTo(lCell); } if (cellsToDraw.size() > 0) { - sb.append(toSixel(x, y, cellsToDraw)); + if (iterm2Images) { + sb.append(toIterm2Image(x, y, cellsToDraw)); + } else if (jexerImageOption != JexerImageOption.DISABLED) { + sb.append(toJexerImage(x, y, cellsToDraw)); + } else { + sb.append(toSixel(x, y, cellsToDraw)); + } } x = right; @@ -1945,6 +2128,7 @@ public class ECMA48Terminal extends LogicalScreen params = new ArrayList(); params.clear(); params.add(""); + decPrivateModeFlag = false; } /** @@ -2062,10 +2246,13 @@ public class ECMA48Terminal extends LogicalScreen boolean eventMouse3 = false; boolean eventMouseWheelUp = false; boolean eventMouseWheelDown = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; // System.err.printf("buttons: %04x\r\n", buttons); - switch (buttons) { + switch (buttons & 0xE3) { case 0: eventMouse1 = true; mouse1 = true; @@ -2147,9 +2334,21 @@ public class ECMA48Terminal extends LogicalScreen eventType = TMouseEvent.Type.MOUSE_MOTION; break; } + + if ((buttons & 0x04) != 0) { + eventShift = true; + } + if ((buttons & 0x08) != 0) { + eventAlt = true; + } + if ((buttons & 0x10) != 0) { + eventCtrl = true; + } + return new TMouseEvent(eventType, x, y, x, y, eventMouse1, eventMouse2, eventMouse3, - eventMouseWheelUp, eventMouseWheelDown); + eventMouseWheelUp, eventMouseWheelDown, + eventAlt, eventCtrl, eventShift); } /** @@ -2184,12 +2383,15 @@ public class ECMA48Terminal extends LogicalScreen boolean eventMouse3 = false; boolean eventMouseWheelUp = false; boolean eventMouseWheelDown = false; + boolean eventAlt = false; + boolean eventCtrl = false; + boolean eventShift = false; if (release) { eventType = TMouseEvent.Type.MOUSE_UP; } - switch (buttons) { + switch (buttons & 0xE3) { case 0: eventMouse1 = true; break; @@ -2246,9 +2448,21 @@ public class ECMA48Terminal extends LogicalScreen // Unknown, bail out return null; } + + if ((buttons & 0x04) != 0) { + eventShift = true; + } + if ((buttons & 0x08) != 0) { + eventAlt = true; + } + if ((buttons & 0x10) != 0) { + eventCtrl = true; + } + return new TMouseEvent(eventType, x, y, x, y, eventMouse1, eventMouse2, eventMouse3, - eventMouseWheelUp, eventMouseWheelDown); + eventMouseWheelUp, eventMouseWheelDown, + eventAlt, eventCtrl, eventShift); } /** @@ -2291,7 +2505,7 @@ public class ECMA48Terminal extends LogicalScreen getTextWidth() + " x " + getTextHeight()); } - this.output.printf("%s", xtermReportWindowPixelDimensions()); + this.output.printf("%s", xtermReportPixelDimensions()); this.output.flush(); TResizeEvent event = new TResizeEvent(TResizeEvent.Type.SCREEN, @@ -2527,6 +2741,10 @@ public class ECMA48Terminal extends LogicalScreen // Mouse position, SGR (1006) coordinates state = ParseState.MOUSE_SGR; return; + case '?': + // DEC private mode flag + decPrivateModeFlag = true; + return; default: break; } @@ -2656,6 +2874,47 @@ public class ECMA48Terminal extends LogicalScreen events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift)); resetParser(); return; + case 'c': + // Device Attributes + if (decPrivateModeFlag == false) { + break; + } + boolean reportsJexerImages = false; + boolean reportsIterm2Images = false; + 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"); + } + reportsJexerImages = true; + } + if (x.equals("1337")) { + // Terminal reports iTerm2 images support + if (debugToStderr) { + System.err.println("Device Attributes: iTerm2 images"); + } + reportsIterm2Images = true; + } + } + if (reportsJexerImages == false) { + // Terminal does not support Jexer images, disable + // them. + jexerImageOption = JexerImageOption.DISABLED; + } + if (reportsIterm2Images == false) { + // Terminal does not support iTerm2 images, disable + // them. + iterm2Images = false; + } + resetParser(); + return; case 't': // windowOps if ((params.size() > 2) && (params.get(0).equals("4"))) { @@ -2679,6 +2938,27 @@ public class ECMA48Terminal extends LogicalScreen heightPixels = 400; } } + if ((params.size() > 2) && (params.get(0).equals("6"))) { + if (debugToStderr) { + System.err.printf("windowOp text cell pixels: " + + "height %s width %s\n", + params.get(1), params.get(2)); + } + try { + widthPixels = width * Integer.parseInt(params.get(2)); + heightPixels = height * Integer.parseInt(params.get(1)); + } catch (NumberFormatException e) { + if (debugToStderr) { + e.printStackTrace(); + } + } + if (widthPixels <= 0) { + widthPixels = 640; + } + if (heightPixels <= 0) { + heightPixels = 400; + } + } resetParser(); return; default: @@ -2708,12 +2988,46 @@ public class ECMA48Terminal extends LogicalScreen } /** - * Request (u)xterm to report the current window size dimensions. + * 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) if sixelSharedPalette is set + * + * @return the string to emit to xterm + */ + private String xtermSetSixelSettings() { + if (sixelSharedPalette == true) { + return "\033[?80h\033[?1070l"; + } else { + return "\033[?80h\033[?1070h"; + } + } + + /** + * 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 xtermReportWindowPixelDimensions() { - return "\033[14t"; + 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"; } /** @@ -2745,6 +3059,46 @@ public class ECMA48Terminal extends LogicalScreen // Sixel output support --------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Get the number of colors in the sixel palette. + * + * @return the palette size + */ + public int getSixelPaletteSize() { + return sixelPaletteSize; + } + + /** + * Set the number of colors in the sixel palette. + * + * @param paletteSize the new palette size + */ + public void setSixelPaletteSize(final int paletteSize) { + if (paletteSize == sixelPaletteSize) { + return; + } + + switch (paletteSize) { + case 2: + case 256: + case 512: + case 1024: + case 2048: + break; + default: + throw new IllegalArgumentException("Unsupported sixel palette " + + " size: " + paletteSize); + } + + // Don't step on the screen refresh thread. + synchronized (this) { + sixelPaletteSize = paletteSize; + palette = null; + sixelCache = null; + clearPhysical(); + } + } + /** * Start a sixel string for display one row's worth of bitmap data. * @@ -2765,6 +3119,9 @@ public class ECMA48Terminal extends LogicalScreen if (palette == null) { palette = new SixelPalette(); + if (sixelSharedPalette == true) { + palette.emitPalette(sb, null); + } } return sb.toString(); @@ -2809,8 +3166,21 @@ public class ECMA48Terminal extends LogicalScreen 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. Do not draw the image, bail out + // instead. + 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 SixelCache(height * 10); + sixelCache = new ImageCache(height * 10); } // Save and get rows to/from the cache that do NOT have inverted @@ -2833,95 +3203,33 @@ public class ECMA48Terminal extends LogicalScreen // 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++) { - if (cells.get(i).isInvertedImage()) { - rgbArray = new int[imageWidth * imageHeight]; - for (int j = 0; j < rgbArray.length; j++) { - rgbArray[j] = 0xFFFFFF; - } - } else { - rgbArray = cells.get(i).getImage().getRGB(0, 0, - imageWidth, imageHeight, null, 0, imageWidth); - } - - /* - 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, imageWidth, imageHeight, - rgbArray, 0, imageWidth); - if (imageHeight < 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 (cells.get(cells.size() - 1).isInvertedImage()) { - rgbArray = new int[totalWidth * imageHeight]; - for (int j = 0; j < rgbArray.length; j++) { - rgbArray[j] = 0xFFFFFF; - } - } else { - rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0, - totalWidth, imageHeight, null, 0, totalWidth); - } - 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); - } - } - } + BufferedImage image = cellsToImage(cells); + int fullHeight = image.getHeight(); // Dither the image. It is ok to lose the original here. if (palette == null) { palette = new SixelPalette(); + if (sixelSharedPalette == true) { + palette.emitPalette(sb, null); + } } image = palette.ditherImage(image); - // Emit the palette, but only for the colors actually used by these - // cells. - boolean [] usedColors = new boolean[MAX_COLOR_REGISTERS]; - for (int imageX = 0; imageX < image.getWidth(); imageX++) { - for (int imageY = 0; imageY < image.getHeight(); imageY++) { - usedColors[image.getRGB(imageX, imageY)] = true; + // Collect the raster information + int rasterHeight = 0; + int rasterWidth = image.getWidth(); + + if (sixelSharedPalette == false) { + // Emit the palette, but only for the colors actually used by + // these cells. + boolean [] usedColors = new boolean[sixelPaletteSize]; + for (int imageX = 0; imageX < image.getWidth(); imageX++) { + for (int imageY = 0; imageY < image.getHeight(); imageY++) { + usedColors[image.getRGB(imageX, imageY)] = true; + } } + palette.emitPalette(sb, usedColors); } - palette.emitPalette(sb, usedColors); // Render the entire row of cells. for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) { @@ -2935,13 +3243,13 @@ public class ECMA48Terminal extends LogicalScreen int colorIdx = image.getRGB(imageX, imageY + currentRow); assert (colorIdx >= 0); - assert (colorIdx < MAX_COLOR_REGISTERS); + assert (colorIdx < sixelPaletteSize); sixels[imageX][imageY] = colorIdx; } } - for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + for (int i = 0; i < sixelPaletteSize; i++) { boolean isUsed = false; for (int imageX = 0; imageX < image.getWidth(); imageX++) { for (int j = 0; j < 6; j++) { @@ -2989,6 +3297,9 @@ public class ECMA48Terminal extends LogicalScreen data += 32; break; } + if ((currentRow + j + 1) > rasterHeight) { + rasterHeight = currentRow + j + 1; + } } } assert (data >= 0); @@ -3018,7 +3329,7 @@ public class ECMA48Terminal extends LogicalScreen sb.append((char) oldData); } - } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++) + } // for (int i = 0; i < sixelPaletteSize; i++) // Advance to the next scan line. sb.append("-"); @@ -3028,6 +3339,9 @@ public class ECMA48Terminal extends LogicalScreen // 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()); @@ -3045,10 +3359,534 @@ public class ECMA48Terminal extends LogicalScreen return sixel; } + /** + * Convert a horizontal range of cell's image data into a single + * contigous image, rescaled and anti-aliased to match the current text + * cell size. + * + * @param cells the cells containing image data + * @return the image resized to the current text cell size + */ + private BufferedImage cellsToImage(final List cells) { + int imageWidth = cells.get(0).getImage().getWidth(); + int imageHeight = cells.get(0).getImage().getHeight(); + + // Piece cells.get(x).getImage() pieces together into one larger + // image for final rendering. + int totalWidth = 0; + int fullWidth = cells.size() * imageWidth; + int fullHeight = imageHeight; + 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 = imageWidth; + int tileHeight = imageHeight; + + if (false && cells.get(i).isInvertedImage()) { + // I used to put an all-white cell over the cursor, don't do + // that anymore. + 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 < imageWidth) { + 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); + } + } + } + + if ((image.getWidth() != cells.size() * getTextWidth()) + || (image.getHeight() != getTextHeight()) + ) { + // Rescale the image to fit the text cells it is going into. + BufferedImage newImage; + newImage = new BufferedImage(cells.size() * getTextWidth(), + getTextHeight(), BufferedImage.TYPE_INT_ARGB); + + Graphics gr = newImage.getGraphics(); + if (gr instanceof Graphics2D) { + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + } + gr.drawImage(image, 0, 0, newImage.getWidth(), + newImage.getHeight(), null, null); + gr.dispose(); + image = newImage; + } + + return image; + } + // ------------------------------------------------------------------------ // 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); + } + + // 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"); + } + + BufferedImage image = cellsToImage(cells); + int fullHeight = image.getHeight(); + + /* + * 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 ""; + } + + 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(StringUtils.toBase64(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 (jexerImageOption == JexerImageOption.DISABLED) { + 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); + } + + // 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"); + } + + BufferedImage image = cellsToImage(cells); + int fullHeight = image.getHeight(); + + if (jexerImageOption == JexerImageOption.PNG) { + // Encode as 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 ""; + } + + sb.append("\033]444;1;0;"); + sb.append(StringUtils.toBase64(pngOutputStream.toByteArray())); + sb.append("\007"); + + } else if (jexerImageOption == JexerImageOption.JPG) { + + // Encode as JPG + ByteArrayOutputStream jpgOutputStream = new ByteArrayOutputStream(1024); + + // Convert from ARGB to RGB, otherwise the JPG encode will fail. + BufferedImage jpgImage = new BufferedImage(image.getWidth(), + image.getHeight(), BufferedImage.TYPE_INT_RGB); + int [] pixels = new int[image.getWidth() * image.getHeight()]; + image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels, + 0, image.getWidth()); + jpgImage.setRGB(0, 0, image.getWidth(), image.getHeight(), pixels, + 0, image.getWidth()); + + try { + if (!ImageIO.write(jpgImage.getSubimage(0, 0, + jpgImage.getWidth(), + Math.min(jpgImage.getHeight(), fullHeight)), + "JPG", jpgOutputStream) + ) { + // We failed to render image, bail out. + return ""; + } + } catch (IOException e) { + // We failed to render image, bail out. + return ""; + } + + sb.append("\033]444;2;0;"); + sb.append(StringUtils.toBase64(jpgOutputStream.toByteArray())); + sb.append("\007"); + + } else if (jexerImageOption == JexerImageOption.RGB) { + + // RGB + sb.append(String.format("\033]444;0;%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(StringUtils.toBase64(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 (jexerImageOption != JexerImageOption.DISABLED); + } + + // ------------------------------------------------------------------------ + // 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. * @@ -3132,21 +3970,21 @@ public class ECMA48Terminal extends LogicalScreen // Bold implies foreground only sb.append("38;2;"); if (color.equals(Color.BLACK)) { - sb.append("84;84;84"); + sb.append(systemColorRGB(MYBOLD_BLACK)); } else if (color.equals(Color.RED)) { - sb.append("252;84;84"); + sb.append(systemColorRGB(MYBOLD_RED)); } else if (color.equals(Color.GREEN)) { - sb.append("84;252;84"); + sb.append(systemColorRGB(MYBOLD_GREEN)); } else if (color.equals(Color.YELLOW)) { - sb.append("252;252;84"); + sb.append(systemColorRGB(MYBOLD_YELLOW)); } else if (color.equals(Color.BLUE)) { - sb.append("84;84;252"); + sb.append(systemColorRGB(MYBOLD_BLUE)); } else if (color.equals(Color.MAGENTA)) { - sb.append("252;84;252"); + sb.append(systemColorRGB(MYBOLD_MAGENTA)); } else if (color.equals(Color.CYAN)) { - sb.append("84;252;252"); + sb.append(systemColorRGB(MYBOLD_CYAN)); } else if (color.equals(Color.WHITE)) { - sb.append("252;252;252"); + sb.append(systemColorRGB(MYBOLD_WHITE)); } } else { if (foreground) { @@ -3155,21 +3993,21 @@ public class ECMA48Terminal extends LogicalScreen sb.append("48;2;"); } if (color.equals(Color.BLACK)) { - sb.append("0;0;0"); + sb.append(systemColorRGB(MYBLACK)); } else if (color.equals(Color.RED)) { - sb.append("168;0;0"); + sb.append(systemColorRGB(MYRED)); } else if (color.equals(Color.GREEN)) { - sb.append("0;168;0"); + sb.append(systemColorRGB(MYGREEN)); } else if (color.equals(Color.YELLOW)) { - sb.append("168;84;0"); + sb.append(systemColorRGB(MYYELLOW)); } else if (color.equals(Color.BLUE)) { - sb.append("0;0;168"); + sb.append(systemColorRGB(MYBLUE)); } else if (color.equals(Color.MAGENTA)) { - sb.append("168;0;168"); + sb.append(systemColorRGB(MYMAGENTA)); } else if (color.equals(Color.CYAN)) { - sb.append("0;168;168"); + sb.append(systemColorRGB(MYCYAN)); } else if (color.equals(Color.WHITE)) { - sb.append("168;168;168"); + sb.append(systemColorRGB(MYWHITE)); } } sb.append("m"); @@ -3406,7 +4244,7 @@ public class ECMA48Terminal extends LogicalScreen } /** - * Create a SGR parameter sequence to reset to defaults. + * Create a SGR parameter sequence to reset to VT100 defaults. * * @return the string to emit to an ANSI / ECMA-style terminal, * e.g. "\033[0m" @@ -3415,6 +4253,29 @@ public class ECMA48Terminal extends LogicalScreen return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK); } + /** + * Create a SGR parameter sequence to reset to ECMA-48 default + * foreground/background. + * + * @return the string to emit to an ANSI / ECMA-style terminal, + * e.g. "\033[0m" + */ + private String defaultColor() { + /* + * VT100 normal. + * Normal (neither bold nor faint). + * Not italicized. + * Not underlined. + * Steady (not blinking). + * Positive (not inverse). + * Visible (not hidden). + * Not crossed-out. + * Default foreground color. + * Default background color. + */ + return "\033[0;22;23;24;25;27;28;29;39;49m"; + } + /** * Create a SGR parameter sequence to reset to defaults. *