+ 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<TInputEvent> 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<TInputEvent> 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<Cell> cells) {
+
+ StringBuilder sb = new StringBuilder();
+
+ assert (cells != null);
+ assert (cells.size() > 0);
+ assert (cells.get(0).getImage() != null);
+
+ if (sixel == false) {
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int i = 0; i < cells.size(); i++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ if (y == height - 1) {
+ // We are on the bottom row. If scrolling mode is enabled
+ // (default), then VT320/xterm will scroll the entire screen if
+ // we draw any pixels here.
+
+ // TODO: support sixel scrolling mode disabled as an option.
+ sb.append(normal());
+ sb.append(gotoXY(x, y));
+ for (int j = 0; j < cells.size(); j++) {
+ sb.append(' ');
+ }
+ return sb.toString();
+ }
+
+ if (sixelCache == null) {
+ sixelCache = new ImageCache(height * 10);
+ }
+
+ // Save and get rows to/from the cache that do NOT have inverted
+ // cells.
+ boolean saveInCache = true;
+ for (Cell cell: cells) {
+ if (cell.isInvertedImage()) {
+ saveInCache = false;
+ }
+ }
+ if (saveInCache) {
+ String cachedResult = sixelCache.get(cells);
+ if (cachedResult != null) {
+ // System.err.println("CACHE HIT");
+ sb.append(startSixel(x, y));
+ sb.append(cachedResult);
+ sb.append(endSixel());
+ return sb.toString();
+ }
+ // System.err.println("CACHE MISS");
+ }
+
+ int imageWidth = cells.get(0).getImage().getWidth();
+ int imageHeight = cells.get(0).getImage().getHeight();
+
+ // cells.get(x).getImage() has a dithered bitmap containing indexes
+ // into the color palette. Piece these together into one larger
+ // image for final rendering.
+ int totalWidth = 0;
+ int fullWidth = cells.size() * getTextWidth();
+ int fullHeight = getTextHeight();
+ for (int i = 0; i < cells.size(); i++) {
+ totalWidth += cells.get(i).getImage().getWidth();
+ }
+
+ BufferedImage image = new BufferedImage(fullWidth,
+ fullHeight, BufferedImage.TYPE_INT_ARGB);
+
+ int [] rgbArray;
+ for (int i = 0; i < cells.size() - 1; i++) {
+ int tileWidth = Math.min(cells.get(i).getImage().getWidth(),
+ imageWidth);
+ int tileHeight = Math.min(cells.get(i).getImage().getHeight(),
+ imageHeight);
+
+ if (false && cells.get(i).isInvertedImage()) {
+ // I used to put an all-white cell over the cursor, don't do
+ // that anymore.
+ rgbArray = new int[imageWidth * imageHeight];
+ for (int j = 0; j < rgbArray.length; j++) {
+ rgbArray[j] = 0xFFFFFF;
+ }
+ } else {
+ try {
+ rgbArray = cells.get(i).getImage().getRGB(0, 0,
+ tileWidth, tileHeight, null, 0, tileWidth);
+ } catch (Exception e) {
+ throw new RuntimeException("image " + imageWidth + "x" +
+ imageHeight +
+ "tile " + tileWidth + "x" +
+ tileHeight +
+ " cells.get(i).getImage() " +
+ cells.get(i).getImage() +
+ " i " + i +
+ " fullWidth " + fullWidth +
+ " fullHeight " + fullHeight, e);
+ }
+ }
+
+ /*
+ System.err.printf("calling image.setRGB(): %d %d %d %d %d\n",
+ i * imageWidth, 0, imageWidth, imageHeight,
+ 0, imageWidth);
+ System.err.printf(" fullWidth %d fullHeight %d cells.size() %d textWidth %d\n",
+ fullWidth, fullHeight, cells.size(), getTextWidth());
+ */
+
+ image.setRGB(i * imageWidth, 0, tileWidth, tileHeight,
+ rgbArray, 0, tileWidth);
+ if (tileHeight < fullHeight) {
+ int backgroundColor = cells.get(i).getBackground().getRGB();
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int imageY = imageHeight; imageY < fullHeight;
+ imageY++) {
+
+ image.setRGB(imageX, imageY, backgroundColor);
+ }
+ }
+ }
+ }
+ totalWidth -= ((cells.size() - 1) * imageWidth);
+ if (false && cells.get(cells.size() - 1).isInvertedImage()) {
+ // I used to put an all-white cell over the cursor, don't do that
+ // anymore.
+ rgbArray = new int[totalWidth * imageHeight];
+ for (int j = 0; j < rgbArray.length; j++) {
+ rgbArray[j] = 0xFFFFFF;
+ }
+ } else {
+ try {
+ rgbArray = cells.get(cells.size() - 1).getImage().getRGB(0, 0,
+ totalWidth, imageHeight, null, 0, totalWidth);
+ } catch (Exception e) {
+ throw new RuntimeException("image " + imageWidth + "x" +
+ imageHeight + " cells.get(cells.size() - 1).getImage() " +
+ cells.get(cells.size() - 1).getImage(), e);
+ }
+ }
+ image.setRGB((cells.size() - 1) * imageWidth, 0, totalWidth,
+ imageHeight, rgbArray, 0, totalWidth);
+
+ if (totalWidth < getTextWidth()) {
+ int backgroundColor = cells.get(cells.size() - 1).getBackground().getRGB();
+
+ for (int imageX = image.getWidth() - totalWidth;
+ imageX < image.getWidth(); imageX++) {
+
+ for (int imageY = 0; imageY < fullHeight; imageY++) {
+ image.setRGB(imageX, imageY, backgroundColor);
+ }
+ }
+ }
+
+ // Dither the image. It is ok to lose the original here.
+ if (palette == null) {
+ palette = new SixelPalette();
+ // TODO: make this an option (shared palette or not)
+ palette.emitPalette(sb, null);
+ }
+ image = palette.ditherImage(image);
+
+ // Collect the raster information
+ int rasterHeight = 0;
+ int rasterWidth = image.getWidth();
+
+ /*
+
+ // TODO: make this an option (shared palette or not)
+
+ // Emit the palette, but only for the colors actually used by these
+ // cells.
+ boolean [] usedColors = new boolean[sixelPaletteSize];
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int imageY = 0; imageY < image.getHeight(); imageY++) {
+ usedColors[image.getRGB(imageX, imageY)] = true;
+ }
+ }
+ palette.emitPalette(sb, usedColors);
+ */
+
+ // Render the entire row of cells.
+ for (int currentRow = 0; currentRow < fullHeight; currentRow += 6) {
+ int [][] sixels = new int[image.getWidth()][6];
+
+ // See which colors are actually used in this band of sixels.
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int imageY = 0;
+ (imageY < 6) && (imageY + currentRow < fullHeight);
+ imageY++) {
+
+ int colorIdx = image.getRGB(imageX, imageY + currentRow);
+ assert (colorIdx >= 0);
+ assert (colorIdx < sixelPaletteSize);
+
+ sixels[imageX][imageY] = colorIdx;
+ }
+ }
+
+ for (int i = 0; i < sixelPaletteSize; i++) {
+ boolean isUsed = false;
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+ for (int j = 0; j < 6; j++) {
+ if (sixels[imageX][j] == i) {
+ isUsed = true;
+ }
+ }
+ }
+ if (isUsed == false) {
+ continue;
+ }
+
+ // Set to the beginning of scan line for the next set of
+ // colored pixels, and select the color.
+ sb.append(String.format("$#%d", i));
+
+ int oldData = -1;
+ int oldDataCount = 0;
+ for (int imageX = 0; imageX < image.getWidth(); imageX++) {
+
+ // Add up all the pixels that match this color.
+ int data = 0;
+ for (int j = 0;
+ (j < 6) && (currentRow + j < fullHeight);
+ j++) {
+
+ if (sixels[imageX][j] == i) {
+ switch (j) {
+ case 0:
+ data += 1;
+ break;
+ case 1:
+ data += 2;
+ break;
+ case 2:
+ data += 4;
+ break;
+ case 3:
+ data += 8;
+ break;
+ case 4:
+ data += 16;
+ break;
+ case 5:
+ data += 32;
+ break;
+ }
+ if ((currentRow + j + 1) > rasterHeight) {
+ rasterHeight = currentRow + j + 1;
+ }
+ }
+ }
+ assert (data >= 0);
+ assert (data < 64);
+ data += 63;
+
+ if (data == oldData) {
+ oldDataCount++;
+ } else {
+ if (oldDataCount == 1) {
+ sb.append((char) oldData);
+ } else if (oldDataCount > 1) {
+ sb.append(String.format("!%d", oldDataCount));
+ sb.append((char) oldData);
+ }
+ oldDataCount = 1;
+ oldData = data;
+ }
+
+ } // for (int imageX = 0; imageX < image.getWidth(); imageX++)
+
+ // Emit the last sequence.
+ if (oldDataCount == 1) {
+ sb.append((char) oldData);
+ } else if (oldDataCount > 1) {
+ sb.append(String.format("!%d", oldDataCount));
+ sb.append((char) oldData);
+ }
+
+ } // for (int i = 0; i < sixelPaletteSize; i++)
+
+ // Advance to the next scan line.
+ sb.append("-");
+
+ } // for (int currentRow = 0; currentRow < imageHeight; currentRow += 6)
+
+ // Kill the very last "-", because it is unnecessary.
+ sb.deleteCharAt(sb.length() - 1);
+
+ // Add the raster information
+ sb.insert(0, String.format("\"1;1;%d;%d", rasterWidth, rasterHeight));
+
+ if (saveInCache) {
+ // This row is OK to save into the cache.
+ sixelCache.put(cells, sb.toString());
+ }
+
+ return (startSixel(x, y) + sb.toString() + endSixel());
+ }
+
+ /**
+ * Get the sixel support flag.
+ *
+ * @return true if this terminal is emitting sixel
+ */
+ public boolean hasSixel() {
+ return sixel;
+ }
+
+ // ------------------------------------------------------------------------
+ // End sixel output support -----------------------------------------------
+ // ------------------------------------------------------------------------
+
+ // ------------------------------------------------------------------------
+ // iTerm2 image output support --------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Create an iTerm2 images string representing a row of several cells
+ * containing bitmap data.
+ *
+ * @param x column coordinate. 0 is the left-most column.
+ * @param y row coordinate. 0 is the top-most row.
+ * @param cells the cells containing the bitmap data
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ private String toIterm2Image(final int x, final int y,
+ final ArrayList<Cell> cells) {
+
+ StringBuilder sb = new StringBuilder();