X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fbackend%2FECMA48Terminal.java;h=3fc6c2092cb38c5aaa2b17b3a61f1e3461a68e29;hb=842be156c1c15b74c1b4bfe30c2866773494a2af;hp=1dc3957d75b534deec2b318a9c2bd501db8d7f5f;hpb=a69ed767c9c07cf35cf1c5f7821fc009cfe79cd2;p=fanfix.git diff --git a/src/jexer/backend/ECMA48Terminal.java b/src/jexer/backend/ECMA48Terminal.java index 1dc3957..3fc6c20 100644 --- a/src/jexer/backend/ECMA48Terminal.java +++ b/src/jexer/backend/ECMA48Terminal.java @@ -44,17 +44,17 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.LinkedList; -import java.util.Map; 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.*; /** @@ -81,12 +81,6 @@ public class ECMA48Terminal extends LogicalScreen MOUSE_SGR, } - /** - * Number of colors in the sixel palette. Xterm 335 defines the max as - * 1024. - */ - private static final int MAX_COLOR_REGISTERS = 1024; - // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -173,6 +167,11 @@ public class ECMA48Terminal extends LogicalScreen */ private TResizeEvent windowResize = null; + /** + * If true, emit wide-char (CJK/Emoji) characters as sixel images. + */ + private boolean wideCharImages = true; + /** * Window width in pixels. Used for sixel support. */ @@ -198,6 +197,13 @@ public class ECMA48Terminal extends LogicalScreen */ private SixelCache sixelCache = null; + /** + * Number of colors in the sixel palette. Xterm 335 defines the max as + * 1024. Valid values are: 2 (black and white), 256, 512, 1024, and + * 2048. + */ + private int sixelPaletteSize = 1024; + /** * If true, then we changed System.in and need to change it back. */ @@ -230,9 +236,27 @@ public class ECMA48Terminal extends LogicalScreen */ private Object listener; + // Colors to map DOS colors to AWT colors. + private static java.awt.Color MYBLACK; + private static java.awt.Color MYRED; + private static java.awt.Color MYGREEN; + private static java.awt.Color MYYELLOW; + private static java.awt.Color MYBLUE; + private static java.awt.Color MYMAGENTA; + private static java.awt.Color MYCYAN; + private static java.awt.Color MYWHITE; + private static java.awt.Color MYBOLD_BLACK; + private static java.awt.Color MYBOLD_RED; + private static java.awt.Color MYBOLD_GREEN; + private static java.awt.Color MYBOLD_YELLOW; + private static java.awt.Color MYBOLD_BLUE; + private static java.awt.Color MYBOLD_MAGENTA; + private static java.awt.Color MYBOLD_CYAN; + private static java.awt.Color MYBOLD_WHITE; + /** * SixelPalette is used to manage the conversion of images between 24-bit - * RGB color and a palette of MAX_COLOR_REGISTERS colors. + * RGB color and a palette of sixelPaletteSize colors. */ private class SixelPalette { @@ -245,7 +269,7 @@ public class ECMA48Terminal extends LogicalScreen * Map of color palette index for sixel output, from the order it was * generated by makePalette() to rgbColors. */ - private int [] rgbSortedIndex = new int[MAX_COLOR_REGISTERS]; + private int [] rgbSortedIndex = new int[sixelPaletteSize]; /** * The color palette, organized by hue, saturation, and luminance. @@ -343,6 +367,16 @@ public class ECMA48Terminal extends LogicalScreen 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]; @@ -415,7 +449,7 @@ public class ECMA48Terminal extends LogicalScreen ((255 - blue) * (255 - blue))) < diff) { // White is a closer match. - idx = MAX_COLOR_REGISTERS - 1; + idx = sixelPaletteSize - 1; } assert (idx != -1); return idx; @@ -438,7 +472,7 @@ public class ECMA48Terminal extends LogicalScreen } /** - * Dither an image to a MAX_COLOR_REGISTERS palette. The dithered + * Dither an image to a sixelPaletteSize palette. The dithered * image cells will contain indexes into the palette. * * @param image the image to dither @@ -461,7 +495,7 @@ public class ECMA48Terminal extends LogicalScreen imageY) & 0xFFFFFF; int colorIdx = matchColor(oldPixel); assert (colorIdx >= 0); - assert (colorIdx < MAX_COLOR_REGISTERS); + assert (colorIdx < sixelPaletteSize); int newPixel = rgbColors.get(colorIdx); ditheredImage.setRGB(imageX, imageY, colorIdx); @@ -480,9 +514,9 @@ public class ECMA48Terminal extends LogicalScreen int red, green, blue; if (imageX < image.getWidth() - 1) { int pXpY = ditheredImage.getRGB(imageX + 1, imageY); - red = (int) ((pXpY >>> 16) & 0xFF) + (7 * redError); - green = (int) ((pXpY >>> 8) & 0xFF) + (7 * greenError); - blue = (int) ( pXpY & 0xFF) + (7 * blueError); + 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); @@ -493,9 +527,9 @@ public class ECMA48Terminal extends LogicalScreen if (imageY < image.getHeight() - 1) { int pXpYp = ditheredImage.getRGB(imageX + 1, imageY + 1); - red = (int) ((pXpYp >>> 16) & 0xFF) + redError; - green = (int) ((pXpYp >>> 8) & 0xFF) + greenError; - blue = (int) ( pXpYp & 0xFF) + blueError; + red = ((pXpYp >>> 16) & 0xFF) + redError; + green = ((pXpYp >>> 8) & 0xFF) + greenError; + blue = ( pXpYp & 0xFF) + blueError; red = clamp(red); green = clamp(green); blue = clamp(blue); @@ -509,9 +543,9 @@ public class ECMA48Terminal extends LogicalScreen int pXYp = ditheredImage.getRGB(imageX, imageY + 1); - red = (int) ((pXmYp >>> 16) & 0xFF) + (3 * redError); - green = (int) ((pXmYp >>> 8) & 0xFF) + (3 * greenError); - blue = (int) ( pXmYp & 0xFF) + (3 * blueError); + 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); @@ -519,9 +553,9 @@ public class ECMA48Terminal extends LogicalScreen pXmYp |= ((green & 0xFF) << 8) | (blue & 0xFF); ditheredImage.setRGB(imageX - 1, imageY + 1, pXmYp); - red = (int) ((pXYp >>> 16) & 0xFF) + (5 * redError); - green = (int) ((pXYp >>> 8) & 0xFF) + (5 * greenError); - blue = (int) ( pXYp & 0xFF) + (5 * blueError); + 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); @@ -659,10 +693,18 @@ 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 (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 @@ -676,13 +718,13 @@ public class ECMA48Terminal extends LogicalScreen satBits = 2; lumBits = 1; - assert (MAX_COLOR_REGISTERS >= 256); - assert ((MAX_COLOR_REGISTERS == 256) - || (MAX_COLOR_REGISTERS == 512) - || (MAX_COLOR_REGISTERS == 1024) - || (MAX_COLOR_REGISTERS == 2048)); + assert (sixelPaletteSize >= 256); + assert ((sixelPaletteSize == 256) + || (sixelPaletteSize == 512) + || (sixelPaletteSize == 1024) + || (sixelPaletteSize == 2048)); - switch (MAX_COLOR_REGISTERS) { + switch (sixelPaletteSize) { case 512: hueBits = 5; satBits = 2; @@ -768,7 +810,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 @@ -780,19 +822,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)); @@ -805,7 +847,7 @@ public class ECMA48Terminal extends LogicalScreen // Set the dimmest color as true black, and the brightest as true // white. rgbColors.set(0, 0); - rgbColors.set(MAX_COLOR_REGISTERS - 1, 0xFFFFFF); + rgbColors.set(sixelPaletteSize - 1, 0xFFFFFF); /* System.err.printf("\n"); @@ -830,7 +872,7 @@ public class ECMA48Terminal extends LogicalScreen public String emitPalette(final StringBuilder sb, final boolean [] used) { - for (int i = 0; i < MAX_COLOR_REGISTERS; i++) { + for (int i = 0; i < sixelPaletteSize; i++) { if (((used != null) && (used[i] == true)) || (used == null)) { int rgbColor = rgbColors.get(i); sb.append(String.format("#%d;2;%d;%d;%d", i, @@ -982,14 +1024,16 @@ public class ECMA48Terminal extends LogicalScreen // ------------------------------------------------------------------------ /** - * Constructor sets up state for getEvent(). + * Constructor sets up state for getEvent(). If either windowWidth or + * windowHeight are less than 1, the terminal is not resized. * * @param listener the object this backend needs to wake up when new * input comes in * @param input an InputStream connected to the remote user, or null for * System.in. If System.in is used, then on non-Windows systems it will - * be put in raw mode; shutdown() will (blindly!) put System.in in cooked - * mode. input is always converted to a Reader with UTF-8 encoding. + * be put in raw mode; closeTerminal() will (blindly!) put System.in in + * cooked mode. input is always converted to a Reader with UTF-8 + * encoding. * @param output an OutputStream connected to the remote user, or null * for System.out. output is always converted to a Writer with UTF-8 * encoding. @@ -1006,10 +1050,12 @@ public class ECMA48Terminal extends LogicalScreen // Send dtterm/xterm sequences, which will probably not work because // allowWindowOps is defaulted to false. - String resizeString = String.format("\033[8;%d;%dt", windowHeight, - windowWidth); - this.output.write(resizeString); - this.output.flush(); + if ((windowWidth > 0) && (windowHeight > 0)) { + String resizeString = String.format("\033[8;%d;%dt", windowHeight, + windowWidth); + this.output.write(resizeString); + this.output.flush(); + } } /** @@ -1019,8 +1065,9 @@ public class ECMA48Terminal extends LogicalScreen * input comes in * @param input an InputStream connected to the remote user, or null for * System.in. If System.in is used, then on non-Windows systems it will - * be put in raw mode; shutdown() will (blindly!) put System.in in cooked - * mode. input is always converted to a Reader with UTF-8 encoding. + * be put in raw mode; closeTerminal() will (blindly!) put System.in in + * cooked mode. input is always converted to a Reader with UTF-8 + * encoding. * @param output an OutputStream connected to the remote user, or null * for System.out. output is always converted to a Writer with UTF-8 * encoding. @@ -1085,26 +1132,10 @@ public class ECMA48Terminal extends LogicalScreen windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); - // Permit RGB colors only if externally requested. - if (System.getProperty("jexer.ECMA48.rgbColor") != null) { - if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) { - doRgbColor = true; - } else { - doRgbColor = false; - } - } - - // Pull the system properties for sixel output. - if (System.getProperty("jexer.ECMA48.sixel") != null) { - if (System.getProperty("jexer.ECMA48.sixel").equals("true")) { - sixel = true; - } else { - sixel = false; - } - } + reloadOptions(); // Spin up the input reader - eventQueue = new LinkedList(); + eventQueue = new ArrayList(); readerThread = new Thread(this); readerThread.start(); @@ -1187,26 +1218,10 @@ public class ECMA48Terminal extends LogicalScreen windowResize = new TResizeEvent(TResizeEvent.Type.SCREEN, sessionInfo.getWindowWidth(), sessionInfo.getWindowHeight()); - // Permit RGB colors only if externally requested - if (System.getProperty("jexer.ECMA48.rgbColor") != null) { - if (System.getProperty("jexer.ECMA48.rgbColor").equals("true")) { - doRgbColor = true; - } else { - doRgbColor = false; - } - } - - // Pull the system properties for sixel output. - if (System.getProperty("jexer.ECMA48.sixel") != null) { - if (System.getProperty("jexer.ECMA48.sixel").equals("true")) { - sixel = true; - } else { - sixel = false; - } - } + reloadOptions(); // Spin up the input reader - eventQueue = new LinkedList(); + eventQueue = new ArrayList(); readerThread = new Thread(this); readerThread.start(); @@ -1270,6 +1285,19 @@ public class ECMA48Terminal extends LogicalScreen 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 --------------------------------------------------------- // ------------------------------------------------------------------------ @@ -1306,7 +1334,7 @@ public class ECMA48Terminal extends LogicalScreen */ public void closeTerminal() { - // System.err.println("=== shutdown() ==="); System.err.flush(); + // System.err.println("=== closeTerminal() ==="); System.err.flush(); // Tell the reader thread to stop looking at input stopReaderThread = true; @@ -1321,7 +1349,7 @@ public class ECMA48Terminal extends LogicalScreen // Disable mouse reporting and show cursor. Defensive null check // here in case closeTerminal() is called twice. if (output != null) { - output.printf("%s%s%s", mouse(false), cursor(true), normal()); + output.printf("%s%s%s", mouse(false), cursor(true), defaultColor()); output.flush(); } @@ -1357,6 +1385,59 @@ public class ECMA48Terminal extends LogicalScreen 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 sixel for full-width characters. + if (System.getProperty("jexer.ECMA48.wideCharImages", + "true").equals("true")) { + wideCharImages = true; + } else { + wideCharImages = false; + } + + // Pull the system properties for sixel output. + if (System.getProperty("jexer.ECMA48.sixel", "true").equals("true")) { + sixel = true; + } else { + sixel = false; + } + + // Palette size + int paletteSize = 1024; + try { + paletteSize = Integer.parseInt(System.getProperty( + "jexer.ECMA48.sixelPaletteSize", "1024")); + switch (paletteSize) { + case 2: + case 256: + case 512: + case 1024: + case 2048: + sixelPaletteSize = paletteSize; + break; + default: + // Ignore value + break; + } + } catch (NumberFormatException e) { + // SQUASH + } + + // Set custom colors + setCustomSystemColors(); + } + // ------------------------------------------------------------------------ // Runnable --------------------------------------------------------------- // ------------------------------------------------------------------------ @@ -1369,7 +1450,7 @@ public class ECMA48Terminal extends LogicalScreen // available() will often return > 1, so we need to read in chunks to // stay caught up. char [] readBuffer = new char[128]; - List events = new LinkedList(); + List events = new ArrayList(); while (!done && !stopReaderThread) { try { @@ -1434,6 +1515,11 @@ public class ECMA48Terminal extends LogicalScreen events.clear(); } + if (output.checkError()) { + // This is EOF. + done = true; + } + // Wait 20 millis for more data Thread.sleep(20); } @@ -1445,6 +1531,17 @@ public class ECMA48Terminal extends LogicalScreen done = true; } } // while ((done == false) && (stopReaderThread == false)) + + // Pass an event up to TApplication to tell it this Backend is done. + synchronized (eventQueue) { + eventQueue.add(new TCommandEvent(cmBackendDisconnect)); + } + if (listener != null) { + synchronized (listener) { + listener.notifyAll(); + } + } + // System.err.println("*** run() exiting..."); System.err.flush(); } @@ -1633,7 +1730,11 @@ public class ECMA48Terminal extends LogicalScreen // Image cell: bypass the rest of the loop, it is not // rendered here. - if (lCell.isImage()) { + if ((wideCharImages && lCell.isImage()) + || (!wideCharImages + && lCell.isImage() + && (lCell.getWidth() == Cell.Width.SINGLE)) + ) { hasImage = true; // Save the last rendered cell @@ -1644,7 +1745,16 @@ public class ECMA48Terminal extends LogicalScreen continue; } - assert (!lCell.isImage()); + assert ((wideCharImages && !lCell.isImage()) + || (!wideCharImages + && (!lCell.isImage() + || (lCell.isImage() + && (lCell.getWidth() != Cell.Width.SINGLE))))); + + if (!wideCharImages && (lCell.getWidth() == Cell.Width.RIGHT)) { + continue; + } + if (hasImage) { hasImage = false; sb.append(gotoXY(x, y)); @@ -1804,7 +1914,13 @@ public class ECMA48Terminal extends LogicalScreen } // Emit the character - sb.append(lCell.getChar()); + if (wideCharImages + // Don't emit the right-half of full-width chars. + || (!wideCharImages + && (lCell.getWidth() != Cell.Width.RIGHT)) + ) { + sb.append(Character.toChars(lCell.getChar())); + } // Save the last rendered cell lastX = x; @@ -1856,7 +1972,10 @@ public class ECMA48Terminal extends LogicalScreen Cell lCell = logical[x][y]; Cell pCell = physical[x][y]; - if (!lCell.isImage()) { + if (!lCell.isImage() + || (!wideCharImages + && (lCell.getWidth() != Cell.Width.SINGLE)) + ) { continue; } @@ -2224,6 +2343,9 @@ public class ECMA48Terminal extends LogicalScreen // 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(); @@ -2232,14 +2354,24 @@ public class ECMA48Terminal extends LogicalScreen || (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()); } - // Request xterm report window dimensions in pixels again. this.output.printf("%s", xtermReportWindowPixelDimensions()); this.output.flush(); @@ -2694,6 +2826,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. * @@ -2745,11 +2917,19 @@ public class ECMA48Terminal extends LogicalScreen StringBuilder sb = new StringBuilder(); - assert (sixel == true); 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 (sixelCache == null) { sixelCache = new SixelCache(height * 10); } @@ -2792,18 +2972,45 @@ public class ECMA48Terminal extends LogicalScreen int [] rgbArray; for (int i = 0; i < cells.size() - 1; i++) { - if (cells.get(i).isInvertedImage()) { + int tileWidth = Math.min(cells.get(i).getImage().getWidth(), + imageWidth); + int tileHeight = Math.min(cells.get(i).getImage().getHeight(), + imageHeight); + if (false && cells.get(i).isInvertedImage()) { + // I used to put an all-white cell over the cursor, don't do + // that anymore. rgbArray = new int[imageWidth * imageHeight]; for (int j = 0; j < rgbArray.length; j++) { rgbArray[j] = 0xFFFFFF; } } else { - rgbArray = cells.get(i).getImage().getRGB(0, 0, - imageWidth, imageHeight, null, 0, imageWidth); + try { + rgbArray = cells.get(i).getImage().getRGB(0, 0, + tileWidth, tileHeight, null, 0, tileWidth); + } catch (Exception e) { + throw new RuntimeException("image " + imageWidth + "x" + + imageHeight + + "tile " + tileWidth + "x" + + tileHeight + + " cells.get(i).getImage() " + + cells.get(i).getImage() + + " i " + i + + " fullWidth " + fullWidth + + " fullHeight " + fullHeight, e); + } } - image.setRGB(i * imageWidth, 0, imageWidth, imageHeight, - rgbArray, 0, imageWidth); - if (imageHeight < fullHeight) { + + /* + 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; @@ -2815,14 +3022,22 @@ public class ECMA48Terminal extends LogicalScreen } } totalWidth -= ((cells.size() - 1) * imageWidth); - if (cells.get(cells.size() - 1).isInvertedImage()) { + if (false && cells.get(cells.size() - 1).isInvertedImage()) { + // I used to put an all-white cell over the cursor, don't do that + // anymore. rgbArray = new int[totalWidth * imageHeight]; for (int j = 0; j < rgbArray.length; j++) { rgbArray[j] = 0xFFFFFF; } } else { - rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0, - totalWidth, imageHeight, null, 0, totalWidth); + try { + rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0, + totalWidth, imageHeight, null, 0, totalWidth); + } catch (Exception e) { + throw new RuntimeException("image " + imageWidth + "x" + + imageHeight + " cells.get(cells.size() - 1).getImage() " + + cells.get(cells.size() - 1).getImage(), e); + } } image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth, imageHeight, rgbArray, 0, totalWidth); @@ -2847,7 +3062,7 @@ public class ECMA48Terminal extends LogicalScreen // Emit the palette, but only for the colors actually used by these // cells. - boolean [] usedColors = new boolean[MAX_COLOR_REGISTERS]; + boolean [] usedColors = new boolean[sixelPaletteSize]; for (int imageX = 0; imageX < image.getWidth(); imageX++) { for (int imageY = 0; imageY < image.getHeight(); imageY++) { usedColors[image.getRGB(imageX, imageY)] = true; @@ -2867,13 +3082,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++) { @@ -2890,6 +3105,8 @@ public class ECMA48Terminal extends LogicalScreen // colored pixels, and select the color. sb.append(String.format("$#%d", i)); + int oldData = -1; + int oldDataCount = 0; for (int imageX = 0; imageX < image.getWidth(); imageX++) { // Add up all the pixels that match this color. @@ -2922,11 +3139,33 @@ public class ECMA48Terminal extends LogicalScreen } } assert (data >= 0); - assert (data < 127); + assert (data < 64); data += 63; - sb.append((char) data); + + if (data == oldData) { + oldDataCount++; + } else { + if (oldDataCount == 1) { + sb.append((char) oldData); + } else if (oldDataCount > 1) { + sb.append(String.format("!%d", oldDataCount)); + sb.append((char) oldData); + } + oldDataCount = 1; + oldData = data; + } + } // for (int imageX = 0; imageX < image.getWidth(); imageX++) - } // for (int i = 0; i < MAX_COLOR_REGISTERS; i++) + + // Emit the last sequence. + if (oldDataCount == 1) { + sb.append((char) oldData); + } else if (oldDataCount > 1) { + sb.append(String.format("!%d", oldDataCount)); + sb.append((char) oldData); + } + + } // for (int i = 0; i < sixelPaletteSize; i++) // Advance to the next scan line. sb.append("-"); @@ -2944,10 +3183,109 @@ public class ECMA48Terminal extends LogicalScreen return (startSixel(x, y) + sb.toString() + endSixel()); } + /** + * Get the sixel support flag. + * + * @return true if this terminal is emitting sixel + */ + public boolean hasSixel() { + return sixel; + } + // ------------------------------------------------------------------------ // End sixel output support ----------------------------------------------- // ------------------------------------------------------------------------ + /** + * Setup system colors to match DOS color palette. + */ + private void setDOSColors() { + MYBLACK = new java.awt.Color(0x00, 0x00, 0x00); + MYRED = new java.awt.Color(0xa8, 0x00, 0x00); + MYGREEN = new java.awt.Color(0x00, 0xa8, 0x00); + MYYELLOW = new java.awt.Color(0xa8, 0x54, 0x00); + MYBLUE = new java.awt.Color(0x00, 0x00, 0xa8); + MYMAGENTA = new java.awt.Color(0xa8, 0x00, 0xa8); + MYCYAN = new java.awt.Color(0x00, 0xa8, 0xa8); + MYWHITE = new java.awt.Color(0xa8, 0xa8, 0xa8); + MYBOLD_BLACK = new java.awt.Color(0x54, 0x54, 0x54); + MYBOLD_RED = new java.awt.Color(0xfc, 0x54, 0x54); + MYBOLD_GREEN = new java.awt.Color(0x54, 0xfc, 0x54); + MYBOLD_YELLOW = new java.awt.Color(0xfc, 0xfc, 0x54); + MYBOLD_BLUE = new java.awt.Color(0x54, 0x54, 0xfc); + MYBOLD_MAGENTA = new java.awt.Color(0xfc, 0x54, 0xfc); + MYBOLD_CYAN = new java.awt.Color(0x54, 0xfc, 0xfc); + MYBOLD_WHITE = new java.awt.Color(0xfc, 0xfc, 0xfc); + } + + /** + * Setup ECMA48 colors to match those provided in system properties. + */ + private void setCustomSystemColors() { + setDOSColors(); + + MYBLACK = getCustomColor("jexer.ECMA48.color0", MYBLACK); + MYRED = getCustomColor("jexer.ECMA48.color1", MYRED); + MYGREEN = getCustomColor("jexer.ECMA48.color2", MYGREEN); + MYYELLOW = getCustomColor("jexer.ECMA48.color3", MYYELLOW); + MYBLUE = getCustomColor("jexer.ECMA48.color4", MYBLUE); + MYMAGENTA = getCustomColor("jexer.ECMA48.color5", MYMAGENTA); + MYCYAN = getCustomColor("jexer.ECMA48.color6", MYCYAN); + MYWHITE = getCustomColor("jexer.ECMA48.color7", MYWHITE); + MYBOLD_BLACK = getCustomColor("jexer.ECMA48.color8", MYBOLD_BLACK); + MYBOLD_RED = getCustomColor("jexer.ECMA48.color9", MYBOLD_RED); + MYBOLD_GREEN = getCustomColor("jexer.ECMA48.color10", MYBOLD_GREEN); + MYBOLD_YELLOW = getCustomColor("jexer.ECMA48.color11", MYBOLD_YELLOW); + MYBOLD_BLUE = getCustomColor("jexer.ECMA48.color12", MYBOLD_BLUE); + MYBOLD_MAGENTA = getCustomColor("jexer.ECMA48.color13", MYBOLD_MAGENTA); + MYBOLD_CYAN = getCustomColor("jexer.ECMA48.color14", MYBOLD_CYAN); + MYBOLD_WHITE = getCustomColor("jexer.ECMA48.color15", MYBOLD_WHITE); + } + + /** + * Setup one system color to match the RGB value provided in system + * properties. + * + * @param key the system property key + * @param defaultColor the default color to return if key is not set, or + * incorrect + * @return a color from the RGB string, or defaultColor + */ + private java.awt.Color getCustomColor(final String key, + final java.awt.Color defaultColor) { + + String rgb = System.getProperty(key); + if (rgb == null) { + return defaultColor; + } + if (rgb.startsWith("#")) { + rgb = rgb.substring(1); + } + int rgbInt = 0; + try { + rgbInt = Integer.parseInt(rgb, 16); + } catch (NumberFormatException e) { + return defaultColor; + } + java.awt.Color color = new java.awt.Color((rgbInt & 0xFF0000) >>> 16, + (rgbInt & 0x00FF00) >>> 8, + (rgbInt & 0x0000FF)); + + return color; + } + + /** + * Create a T.416 RGB parameter sequence for a custom system color. + * + * @param color one of the MYBLACK, MYBOLD_BLUE, etc. colors + * @return the color portion of the string to emit to an ANSI / + * ECMA-style terminal + */ + private String systemColorRGB(final java.awt.Color color) { + return String.format("%d;%d;%d", color.getRed(), color.getGreen(), + color.getBlue()); + } + /** * Create a SGR parameter sequence for a single color change. * @@ -3031,21 +3369,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) { @@ -3054,21 +3392,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"); @@ -3305,7 +3643,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" @@ -3314,6 +3652,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. * @@ -3388,6 +3749,11 @@ public class ECMA48Terminal extends LogicalScreen * * 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. @@ -3395,9 +3761,9 @@ public class ECMA48Terminal extends LogicalScreen */ private String mouse(final boolean on) { if (on) { - return "\033[?1002;1003;1005;1006h\033[?1049h"; + return "\033[?1002;1003;1005;1006h\033[?1049h\033^hideMousePointer\033\\"; } - return "\033[?1002;1003;1006;1005l\033[?1049l"; + return "\033[?1002;1003;1006;1005l\033[?1049l\033^showMousePointer\033\\"; } }