Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[nikiroo-utils.git] / src / jexer / tterminal / ECMA48.java
index c94d7d2b8524f60290496f53e4b97cbe0bdec883..1d3481169cc5300c5134c652e7745a962f42a2ec 100644 (file)
@@ -47,10 +47,14 @@ import java.util.HashMap;
 import java.util.List;
 
 import jexer.TKeypress;
-import jexer.event.TMouseEvent;
+import jexer.backend.GlyphMaker;
 import jexer.bits.Color;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.bits.StringUtils;
+import jexer.event.TInputEvent;
+import jexer.event.TKeypressEvent;
+import jexer.event.TMouseEvent;
 import jexer.io.ReadTimeoutException;
 import jexer.io.TimeoutInputStream;
 import static jexer.TKeypress.*;
@@ -346,7 +350,7 @@ public class ECMA48 implements Runnable {
     /**
      * Last character printed.
      */
-    private char repCh;
+    private int repCh;
 
     /**
      * VT100-style line wrapping: a character is placed in column 80 (or
@@ -467,6 +471,11 @@ public class ECMA48 implements Runnable {
      */
     private StringBuilder sixelParseBuffer;
 
+    /**
+     * Sixel shared palette.
+     */
+    private HashMap<Integer, java.awt.Color> sixelPalette;
+
     /**
      * The width of a character cell in pixels.
      */
@@ -477,6 +486,23 @@ public class ECMA48 implements Runnable {
      */
     private int textHeight = 20;
 
+    /**
+     * The last used height of a character cell in pixels, only used for
+     * full-width chars.
+     */
+    private int lastTextHeight = -1;
+
+    /**
+     * The glyph drawer for full-width chars.
+     */
+    private GlyphMaker glyphMaker = null;
+
+    /**
+     * Input queue for keystrokes and mouse events to send to the remote
+     * side.
+     */
+    private ArrayList<TInputEvent> userQueue = new ArrayList<TInputEvent>();
+
     /**
      * DECSC/DECRC save/restore a subset of the total state.  This class
      * encapsulates those specific flags/modes.
@@ -669,12 +695,18 @@ public class ECMA48 implements Runnable {
         char [] readBufferUTF8 = null;
         byte [] readBuffer = null;
         if (utf8) {
-            readBufferUTF8 = new char[128];
+            readBufferUTF8 = new char[2048];
         } else {
-            readBuffer = new byte[128];
+            readBuffer = new byte[2048];
         }
 
         while (!done && !stopReaderThread) {
+            synchronized (userQueue) {
+                while (userQueue.size() > 0) {
+                    handleUserEvent(userQueue.remove(0));
+                }
+            }
+
             try {
                 int n = inputStream.available();
 
@@ -696,7 +728,7 @@ public class ECMA48 implements Runnable {
                 }
                 if (n == 0) {
                     try {
-                        Thread.sleep(2);
+                        Thread.sleep(10);
                     } catch (InterruptedException e) {
                         // SQUASH
                     }
@@ -723,15 +755,17 @@ public class ECMA48 implements Runnable {
                 } else {
                     // Don't step on UI events
                     synchronized (this) {
-                        for (int i = 0; i < rc; i++) {
-                            int ch = 0;
-                            if (utf8) {
-                                ch = readBufferUTF8[i];
-                            } else {
-                                ch = readBuffer[i];
+                        if (utf8) {
+                            for (int i = 0; i < rc;) {
+                                int ch = Character.codePointAt(readBufferUTF8,
+                                    i);
+                                i += Character.charCount(ch);
+                                consume(ch);
+                            }
+                        } else {
+                            for (int i = 0; i < rc; i++) {
+                                consume(readBuffer[i]);
                             }
-
-                            consume((char) ch);
                         }
                     }
                     // Permit my enclosing UI to know that I updated.
@@ -797,6 +831,31 @@ public class ECMA48 implements Runnable {
     // ECMA48 -----------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Process keyboard and mouse events from the user.
+     *
+     * @param event the input event to consume
+     */
+    private void handleUserEvent(final TInputEvent event) {
+        if (event instanceof TKeypressEvent) {
+            keypress(((TKeypressEvent) event).getKey());
+        }
+        if (event instanceof TMouseEvent) {
+            mouse((TMouseEvent) event);
+        }
+    }
+
+    /**
+     * Add a keyboard and mouse event from the user to the queue.
+     *
+     * @param event the input event to consume
+     */
+    public void addUserEvent(final TInputEvent event) {
+        synchronized (userQueue) {
+            userQueue.add(event);
+        }
+    }
+
     /**
      * Return the proper primary Device Attributes string.
      *
@@ -817,10 +876,12 @@ public class ECMA48 implements Runnable {
         case XTERM:
             // "I am a VT220" - 7 bit version
             if (!s8c1t) {
-                return "\033[?62;1;6c";
+                return "\033[?62;1;6;9;4;22c";
+                // return "\033[?62;1;6;9;4;22;444c";
             }
             // "I am a VT220" - 8 bit version
-            return "\u009b?62;1;6c";
+            return "\u009b?62;1;6;9;4;22c";
+            // return "\u009b?62;1;6;9;4;22;444c";
         default:
             throw new IllegalArgumentException("Invalid device type: " + type);
         }
@@ -1024,6 +1085,64 @@ public class ECMA48 implements Runnable {
         return display;
     }
 
+    /**
+     * Get the visible display + scrollback buffer, offset by a specified
+     * number of rows from the bottom.
+     *
+     * @param visibleHeight the total height of the display to show
+     * @param scrollBottom the number of rows from the bottom to scroll back
+     * @return a copy of the display + scrollback buffers
+     */
+    public final List<DisplayLine> getVisibleDisplay(final int visibleHeight,
+        final int scrollBottom) {
+
+        assert (visibleHeight >= 0);
+        assert (scrollBottom >= 0);
+
+        int visibleBottom = scrollback.size() + display.size() - scrollBottom;
+
+        List<DisplayLine> preceedingBlankLines = new ArrayList<DisplayLine>();
+        int visibleTop = visibleBottom - visibleHeight;
+        if (visibleTop < 0) {
+            for (int i = visibleTop; i < 0; i++) {
+                preceedingBlankLines.add(getBlankDisplayLine());
+            }
+            visibleTop = 0;
+        }
+        assert (visibleTop >= 0);
+
+        List<DisplayLine> displayLines = new ArrayList<DisplayLine>();
+        displayLines.addAll(scrollback);
+        displayLines.addAll(display);
+
+        List<DisplayLine> visibleLines = new ArrayList<DisplayLine>();
+        visibleLines.addAll(preceedingBlankLines);
+        visibleLines.addAll(displayLines.subList(visibleTop, visibleBottom));
+
+        // Fill in the blank lines on bottom
+        int bottomBlankLines = visibleHeight - visibleLines.size();
+        assert (bottomBlankLines >= 0);
+        for (int i = 0; i < bottomBlankLines; i++) {
+            visibleLines.add(getBlankDisplayLine());
+        }
+
+        return copyBuffer(visibleLines);
+    }
+
+    /**
+     * Copy a display buffer.
+     *
+     * @param buffer the buffer to copy
+     * @return a deep copy of the buffer's data
+     */
+    private List<DisplayLine> copyBuffer(final List<DisplayLine> buffer) {
+        ArrayList<DisplayLine> result = new ArrayList<DisplayLine>(buffer.size());
+        for (DisplayLine line: buffer) {
+            result.add(new DisplayLine(line));
+        }
+        return result;
+    }
+
     /**
      * Get the display width.
      *
@@ -1038,7 +1157,7 @@ public class ECMA48 implements Runnable {
      *
      * @param width the new width
      */
-    public final void setWidth(final int width) {
+    public final synchronized void setWidth(final int width) {
         this.width = width;
         rightMargin = width - 1;
         if (currentState.cursorX >= width) {
@@ -1063,7 +1182,7 @@ public class ECMA48 implements Runnable {
      *
      * @param height the new height
      */
-    public final void setHeight(final int height) {
+    public final synchronized void setHeight(final int height) {
         int delta = height - this.height;
         this.height = height;
         scrollRegionBottom += delta;
@@ -1374,9 +1493,38 @@ public class ECMA48 implements Runnable {
      *
      * @param ch character to display
      */
-    private void printCharacter(final char ch) {
+    private void printCharacter(final int ch) {
         int rightMargin = this.rightMargin;
 
+        if (StringUtils.width(ch) == 2) {
+            // This is a full-width character.  Save two spaces, and then
+            // draw the character as two image halves.
+            int x0 = currentState.cursorX;
+            int y0 = currentState.cursorY;
+            printCharacter(' ');
+            printCharacter(' ');
+            if ((currentState.cursorX == x0 + 2)
+                && (currentState.cursorY == y0)
+            ) {
+                // We can draw both halves of the character.
+                drawHalves(x0, y0, x0 + 1, y0, ch);
+            } else if ((currentState.cursorX == x0 + 1)
+                && (currentState.cursorY == y0)
+            ) {
+                // VT100 line wrap behavior: we should be at the right
+                // margin.  We can draw both halves of the character.
+                drawHalves(x0, y0, x0 + 1, y0, ch);
+            } else {
+                // The character splits across the line.  Draw the entire
+                // character on the new line, giving one more space for it.
+                x0 = currentState.cursorX - 1;
+                y0 = currentState.cursorY;
+                printCharacter(' ');
+                drawHalves(x0, y0, x0 + 1, y0, ch);
+            }
+            return;
+        }
+
         // Check if we have double-width, and if so chop at 40/66 instead of
         // 80/132
         if (display.get(currentState.cursorY).isDoubleWidth()) {
@@ -1427,19 +1575,22 @@ public class ECMA48 implements Runnable {
         CellAttributes newCellAttributes = (CellAttributes) newCell;
         newCellAttributes.setTo(currentState.attr);
         DisplayLine line = display.get(currentState.cursorY);
-        // Insert mode special case
-        if (insertMode == true) {
-            line.insert(currentState.cursorX, newCell);
-        } else {
-            // Replace an existing character
-            line.replace(currentState.cursorX, newCell);
-        }
 
-        // Increment horizontal
-        if (wrapLineFlag == false) {
-            currentState.cursorX++;
-            if (currentState.cursorX > rightMargin) {
-                currentState.cursorX--;
+        if (StringUtils.width(ch) == 1) {
+            // Insert mode special case
+            if (insertMode == true) {
+                line.insert(currentState.cursorX, newCell);
+            } else {
+                // Replace an existing character
+                line.replace(currentState.cursorX, newCell);
+            }
+
+            // Increment horizontal
+            if (wrapLineFlag == false) {
+                currentState.cursorX++;
+                if (currentState.cursorX > rightMargin) {
+                    currentState.cursorX--;
+                }
             }
         }
     }
@@ -1450,7 +1601,7 @@ public class ECMA48 implements Runnable {
      *
      * @param mouse mouse event received from the local user
      */
-    public void mouse(final TMouseEvent mouse) {
+    private void mouse(final TMouseEvent mouse) {
 
         /*
         System.err.printf("mouse(): protocol %s encoding %s mouse %s\n",
@@ -1598,7 +1749,7 @@ public class ECMA48 implements Runnable {
      *
      * @param keypress keypress received from the local user
      */
-    public void keypress(final TKeypress keypress) {
+    private void keypress(final TKeypress keypress) {
         writeRemote(keypressToString(keypress));
     }
 
@@ -1657,11 +1808,14 @@ public class ECMA48 implements Runnable {
              * the remote side.
              */
             if (keypress.getChar() < 0x20) {
-                handleControlChar(keypress.getChar());
+                handleControlChar((char) keypress.getChar());
             } else {
                 // Local echo for everything else
                 printCharacter(keypress.getChar());
             }
+            if (displayListener != null) {
+                displayListener.displayChanged();
+            }
         }
 
         if ((newLineMode == true) && (keypress.equals(kbEnter))) {
@@ -1672,17 +1826,17 @@ public class ECMA48 implements Runnable {
         // Handle control characters
         if ((keypress.isCtrl()) && (!keypress.isFnKey())) {
             StringBuilder sb = new StringBuilder();
-            char ch = keypress.getChar();
+            int ch = keypress.getChar();
             ch -= 0x40;
-            sb.append(ch);
+            sb.append(Character.toChars(ch));
             return sb.toString();
         }
 
         // Handle alt characters
         if ((keypress.isAlt()) && (!keypress.isFnKey())) {
             StringBuilder sb = new StringBuilder("\033");
-            char ch = keypress.getChar();
-            sb.append(ch);
+            int ch = keypress.getChar();
+            sb.append(Character.toChars(ch));
             return sb.toString();
         }
 
@@ -2234,7 +2388,7 @@ public class ECMA48 implements Runnable {
         // Non-alt, non-ctrl characters
         if (!keypress.isFnKey()) {
             StringBuilder sb = new StringBuilder();
-            sb.append(keypress.getChar());
+            sb.append(Character.toChars(keypress.getChar()));
             return sb.toString();
         }
         return "";
@@ -2249,7 +2403,7 @@ public class ECMA48 implements Runnable {
      * @param charsetGr character set defined for GR
      * @return character to display on the screen
      */
-    private char mapCharacterCharset(final char ch,
+    private char mapCharacterCharset(final int ch,
         final CharacterSet charsetGl,
         final CharacterSet charsetGr) {
 
@@ -2327,7 +2481,7 @@ public class ECMA48 implements Runnable {
      * @param ch either 8-bit or Unicode character from the remote side
      * @return character to display on the screen
      */
-    private char mapCharacter(final char ch) {
+    private int mapCharacter(final int ch) {
         if (ch >= 0x100) {
             // Unicode character, just return it
             return ch;
@@ -2730,6 +2884,7 @@ public class ECMA48 implements Runnable {
      */
     private void setToggle(final boolean value) {
         boolean decPrivateModeFlag = false;
+
         for (int i = 0; i < collectBuffer.length(); i++) {
             if (collectBuffer.charAt(i) == '?') {
                 decPrivateModeFlag = true;
@@ -2996,6 +3151,21 @@ public class ECMA48 implements Runnable {
 
                 break;
 
+            case 80:
+                if (type == DeviceType.XTERM) {
+                    if (decPrivateModeFlag == true) {
+                        if (value == true) {
+                            // Enable sixel scrolling (default).
+                            // TODO
+                        } else {
+                            // Disable sixel scrolling.
+                            // TODO
+                        }
+                    }
+                }
+
+                break;
+
             case 1000:
                 if ((type == DeviceType.XTERM)
                     && (decPrivateModeFlag == true)
@@ -3061,6 +3231,22 @@ public class ECMA48 implements Runnable {
                 }
                 break;
 
+            case 1070:
+                if (type == DeviceType.XTERM) {
+                    if (decPrivateModeFlag == true) {
+                        if (value == true) {
+                            // Use private color registers for each sixel
+                            // graphic (default).
+                            sixelPalette = null;
+                        } else {
+                            // Use shared color registers for each sixel
+                            // graphic.
+                            sixelPalette = new HashMap<Integer, java.awt.Color>();
+                        }
+                    }
+                }
+                break;
+
             default:
                 break;
 
@@ -3374,8 +3560,7 @@ public class ECMA48 implements Runnable {
      * DECALN - Screen alignment display.
      */
     private void decaln() {
-        Cell newCell = new Cell();
-        newCell.setChar('E');
+        Cell newCell = new Cell('E');
         for (DisplayLine line: display) {
             for (int i = 0; i < line.length(); i++) {
                 line.replace(i, newCell);
@@ -4580,6 +4765,38 @@ public class ECMA48 implements Runnable {
                         }
                     }
                 }
+
+                if (p[0].equals("10")) {
+                    if (p[1].equals("?")) {
+                        // Respond with foreground color.
+                        java.awt.Color color = jexer.backend.SwingTerminal.attrToForegroundColor(currentState.attr);
+
+                        writeRemote(String.format(
+                            "\033]10;rgb:%04x/%04x/%04x\033\\",
+                                color.getRed() << 8,
+                                color.getGreen() << 8,
+                                color.getBlue() << 8));
+                    }
+                }
+
+                if (p[0].equals("11")) {
+                    if (p[1].equals("?")) {
+                        // Respond with background color.
+                        java.awt.Color color = jexer.backend.SwingTerminal.attrToBackgroundColor(currentState.attr);
+
+                        writeRemote(String.format(
+                            "\033]11;rgb:%04x/%04x/%04x\033\\",
+                                color.getRed() << 8,
+                                color.getGreen() << 8,
+                                color.getBlue() << 8));
+                    }
+                }
+
+                if (p[0].equals("444") && (p.length == 5)) {
+                    // Jexer image
+                    parseJexerImage(p[1], p[2], p[3], p[4]);
+                }
+
             }
 
             // Go to SCAN_GROUND state
@@ -4636,27 +4853,66 @@ public class ECMA48 implements Runnable {
         int i = getCsiParam(0, 0);
 
         if (!xtermPrivateModeFlag) {
-            if (i == 14) {
-                // Report xterm window in pixels as CSI 4 ; height ; width t
+            switch (i) {
+            case 14:
+                // Report xterm text area size in pixels as CSI 4 ; height ;
+                // width t
                 writeRemote(String.format("\033[4;%d;%dt", textHeight * height,
                         textWidth * width));
+                break;
+            case 16:
+                // Report character size in pixels as CSI 6 ; height ; width
+                // t
+                writeRemote(String.format("\033[6;%d;%dt", textHeight,
+                        textWidth));
+                break;
+            case 18:
+                // Report the text are size in characters as CSI 8 ; height ;
+                // width t
+                writeRemote(String.format("\033[8;%d;%dt", height, width));
+                break;
+            default:
+                break;
             }
         }
     }
 
+    /**
+     * Respond to xterm sixel query.
+     */
+    private void xtermSixelQuery() {
+        int item = getCsiParam(0, 0);
+        int action = getCsiParam(1, 0);
+        int value = getCsiParam(2, 0);
+
+        switch (item) {
+        case 1:
+            if (action == 1) {
+                // Report number of color registers.
+                writeRemote(String.format("\033[?%d;%d;%dS", item, 0, 1024));
+                return;
+            }
+            break;
+        default:
+            break;
+        }
+        // We will not support this option.
+        writeRemote(String.format("\033[?%d;%dS", item, action));
+    }
+
     /**
      * Run this input character through the ECMA48 state machine.
      *
      * @param ch character from the remote side
      */
-    private void consume(char ch) {
+    private void consume(int ch) {
 
         // DEBUG
         // System.err.printf("%c STATE = %s\n", ch, scanState);
 
         // Special case for VT10x: 7-bit characters only
         if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) {
-            ch = (char)(ch & 0x7F);
+            ch = (ch & 0x7F);
         }
 
         // Special "anywhere" states
@@ -4726,7 +4982,7 @@ public class ECMA48 implements Runnable {
             // 00-17, 19, 1C-1F --> execute
             // 80-8F, 91-9A, 9C --> execute
             if ((ch <= 0x1F) || ((ch >= 0x80) && (ch <= 0x9F))) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-7F            --> print
@@ -4753,13 +5009,13 @@ public class ECMA48 implements Runnable {
         case ESCAPE:
             // 00-17, 19, 1C-1F --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
                 return;
             }
 
             // 20-2F            --> collect, then switch to ESCAPE_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.ESCAPE_INTERMEDIATE;
                 return;
             }
@@ -5095,12 +5351,12 @@ public class ECMA48 implements Runnable {
         case ESCAPE_INTERMEDIATE:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
             }
 
             // 30-7E               --> dispatch, then switch to GROUND
@@ -5702,12 +5958,12 @@ public class ECMA48 implements Runnable {
         case CSI_ENTRY:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect, then switch to CSI_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.CSI_INTERMEDIATE;
             }
 
@@ -5723,7 +5979,7 @@ public class ECMA48 implements Runnable {
 
             // 3C-3F               --> collect, then switch to CSI_PARAM
             if ((ch >= 0x3C) && (ch <= 0x3F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.CSI_PARAM;
             }
 
@@ -5807,7 +6063,18 @@ public class ECMA48 implements Runnable {
                 case 'S':
                     // Scroll up X lines (default 1)
                     if (type == DeviceType.XTERM) {
-                        su();
+                        boolean xtermPrivateModeFlag = false;
+                        for (int i = 0; i < collectBuffer.length(); i++) {
+                            if (collectBuffer.charAt(i) == '?') {
+                                xtermPrivateModeFlag = true;
+                                break;
+                            }
+                        }
+                        if (xtermPrivateModeFlag) {
+                            xtermSixelQuery();
+                        } else {
+                            su();
+                        }
                     }
                     break;
                 case 'T':
@@ -5975,12 +6242,12 @@ public class ECMA48 implements Runnable {
         case CSI_PARAM:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect, then switch to CSI_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.CSI_INTERMEDIATE;
             }
 
@@ -6081,7 +6348,18 @@ public class ECMA48 implements Runnable {
                 case 'S':
                     // Scroll up X lines (default 1)
                     if (type == DeviceType.XTERM) {
-                        su();
+                        boolean xtermPrivateModeFlag = false;
+                        for (int i = 0; i < collectBuffer.length(); i++) {
+                            if (collectBuffer.charAt(i) == '?') {
+                                xtermPrivateModeFlag = true;
+                                break;
+                            }
+                        }
+                        if (xtermPrivateModeFlag) {
+                            xtermSixelQuery();
+                        } else {
+                            su();
+                        }
                     }
                     break;
                 case 'T':
@@ -6229,12 +6507,12 @@ public class ECMA48 implements Runnable {
         case CSI_INTERMEDIATE:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
             }
 
             // 0x30-3F goes to CSI_IGNORE
@@ -6342,12 +6620,12 @@ public class ECMA48 implements Runnable {
         case CSI_IGNORE:
             // 00-17, 19, 1C-1F    --> execute
             if (ch <= 0x1F) {
-                handleControlChar(ch);
+                handleControlChar((char) ch);
             }
 
             // 20-2F               --> collect
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
             }
 
             // 40-7E               --> ignore, then switch to GROUND
@@ -6368,7 +6646,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6380,7 +6658,7 @@ public class ECMA48 implements Runnable {
 
             // 20-2F               --> collect, then switch to DCS_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.DCS_INTERMEDIATE;
             }
 
@@ -6396,7 +6674,7 @@ public class ECMA48 implements Runnable {
 
             // 3C-3F               --> collect, then switch to DCS_PARAM
             if ((ch >= 0x3C) && (ch <= 0x3F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.DCS_PARAM;
             }
 
@@ -6426,7 +6704,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6458,7 +6736,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6470,7 +6748,7 @@ public class ECMA48 implements Runnable {
 
             // 20-2F          --> collect, then switch to DCS_INTERMEDIATE
             if ((ch >= 0x20) && (ch <= 0x2F)) {
-                collect(ch);
+                collect((char) ch);
                 scanState = ScanState.DCS_INTERMEDIATE;
             }
 
@@ -6510,7 +6788,7 @@ public class ECMA48 implements Runnable {
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6521,17 +6799,20 @@ public class ECMA48 implements Runnable {
             }
 
             // 00-17, 19, 1C-1F, 20-7E   --> put
-            // TODO
             if (ch <= 0x17) {
+                // We ignore all DCS except sixel.
                 return;
             }
             if (ch == 0x19) {
+                // We ignore all DCS except sixel.
                 return;
             }
             if ((ch >= 0x1C) && (ch <= 0x1F)) {
+                // We ignore all DCS except sixel.
                 return;
             }
             if ((ch >= 0x20) && (ch <= 0x7E)) {
+                // We ignore all DCS except sixel.
                 return;
             }
 
@@ -6554,11 +6835,13 @@ public class ECMA48 implements Runnable {
             if (ch == 0x9C) {
                 parseSixel();
                 toGround();
+                return;
             }
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
-                collect(ch);
+                collect((char) ch);
+                return;
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6566,29 +6849,20 @@ public class ECMA48 implements Runnable {
                 ) {
                     parseSixel();
                     toGround();
+                    return;
                 }
             }
 
             // 00-17, 19, 1C-1F, 20-7E   --> put
-            if (ch <= 0x17) {
-                sixelParseBuffer.append(ch);
-                return;
-            }
-            if (ch == 0x19) {
-                sixelParseBuffer.append(ch);
-                return;
-            }
-            if ((ch >= 0x1C) && (ch <= 0x1F)) {
-                sixelParseBuffer.append(ch);
-                return;
-            }
-            if ((ch >= 0x20) && (ch <= 0x7E)) {
-                sixelParseBuffer.append(ch);
-                return;
+            if ((ch <= 0x17)
+                || (ch == 0x19)
+                || ((ch >= 0x1C) && (ch <= 0x1F))
+                || ((ch >= 0x20) && (ch <= 0x7E))
+            ) {
+                sixelParseBuffer.append((char) ch);
             }
 
             // 7F                        --> ignore
-
             return;
 
         case SOSPMAPC_STRING:
@@ -6596,11 +6870,11 @@ public class ECMA48 implements Runnable {
 
             // Special case for Jexer: PM can pass one control character
             if (ch == 0x1B) {
-                pmPut(ch);
+                pmPut((char) ch);
             }
 
             if ((ch >= 0x20) && (ch <= 0x7F)) {
-                pmPut(ch);
+                pmPut((char) ch);
             }
 
             // 0x9C goes to GROUND
@@ -6613,14 +6887,14 @@ public class ECMA48 implements Runnable {
         case OSC_STRING:
             // Special case for Xterm: OSC can pass control characters
             if ((ch == 0x9C) || (ch == 0x07) || (ch == 0x1B)) {
-                oscPut(ch);
+                oscPut((char) ch);
             }
 
             // 00-17, 19, 1C-1F        --> ignore
 
             // 20-7F                   --> osc_put
             if ((ch >= 0x20) && (ch <= 0x7F)) {
-                oscPut(ch);
+                oscPut((char) ch);
             }
 
             // 0x9C goes to GROUND
@@ -6633,7 +6907,7 @@ public class ECMA48 implements Runnable {
         case VT52_DIRECT_CURSOR_ADDRESS:
             // This is a special case for the VT52 sequence "ESC Y l c"
             if (collectBuffer.length() == 0) {
-                collect(ch);
+                collect((char) ch);
             } else if (collectBuffer.length() == 1) {
                 // We've got the two characters, one in the buffer and the
                 // other in ch.
@@ -6686,9 +6960,43 @@ public class ECMA48 implements Runnable {
         return mouseProtocol;
     }
 
-    // ------------------------------------------------------------------------
-    // Sixel support ----------------------------------------------------------
-    // ------------------------------------------------------------------------
+    /**
+     * Draw the left and right cells of a two-cell-wide (full-width) glyph.
+     *
+     * @param leftX the x position to draw the left half to
+     * @param leftY the y position to draw the left half to
+     * @param rightX the x position to draw the right half to
+     * @param rightY the y position to draw the right half to
+     * @param ch the character to draw
+     */
+    private void drawHalves(final int leftX, final int leftY,
+        final int rightX, final int rightY, final int ch) {
+
+        // System.err.println("drawHalves(): " + Integer.toHexString(ch));
+
+        if (lastTextHeight != textHeight) {
+            glyphMaker = GlyphMaker.getInstance(textHeight);
+            lastTextHeight = textHeight;
+        }
+
+        Cell cell = new Cell(ch, currentState.attr);
+        BufferedImage image = glyphMaker.getImage(cell, textWidth * 2,
+            textHeight);
+        BufferedImage leftImage = image.getSubimage(0, 0, textWidth,
+            textHeight);
+        BufferedImage rightImage = image.getSubimage(textWidth, 0, textWidth,
+            textHeight);
+
+        Cell left = new Cell(cell);
+        left.setImage(leftImage);
+        left.setWidth(Cell.Width.LEFT);
+        display.get(leftY).replace(leftX, left);
+
+        Cell right = new Cell(cell);
+        right.setImage(rightImage);
+        right.setWidth(Cell.Width.RIGHT);
+        display.get(rightY).replace(rightX, right);
+    }
 
     /**
      * Set the width of a character cell in pixels.
@@ -6717,9 +7025,9 @@ public class ECMA48 implements Runnable {
         /*
         System.err.println("parseSixel(): '" + sixelParseBuffer.toString()
             + "'");
-         */
+        */
 
-        Sixel sixel = new Sixel(sixelParseBuffer.toString());
+        Sixel sixel = new Sixel(sixelParseBuffer.toString(), sixelPalette);
         BufferedImage image = sixel.getImage();
 
         // System.err.println("parseSixel(): image " + image);
@@ -6786,19 +7094,20 @@ public class ECMA48 implements Runnable {
         int x0 = currentState.cursorX;
         for (int y = 0; y < cellRows; y++) {
             for (int x = 0; x < cellColumns; x++) {
-                printCharacter(' ');
-                cursorLeft(1, false);
-                if ((x == cellColumns - 1) || (y == cellRows - 1)) {
-                    // TODO: render text of current cell first, then image
-                    // over it.  For now, just copy the cell.
-                    DisplayLine line = display.get(currentState.cursorY);
-                    line.replace(currentState.cursorX, cells[x][y]);
-                } else {
-                    // Copy the image cell into the display.
-                    DisplayLine line = display.get(currentState.cursorY);
-                    line.replace(currentState.cursorX, cells[x][y]);
+                assert (currentState.cursorX <= rightMargin);
+
+                // TODO: Render text of current cell first, then image over
+                // it (accounting for blank pixels).  For now, just copy the
+                // cell.
+                DisplayLine line = display.get(currentState.cursorY);
+                line.replace(currentState.cursorX, cells[x][y]);
+
+                // If at the end of the visible screen, stop.
+                if (currentState.cursorX == rightMargin) {
+                    break;
                 }
-                cursorRight(1, false);
+                // Room for more image on the visible screen.
+                currentState.cursorX++;
             }
             linefeed();
             cursorPosition(currentState.cursorY, x0);
@@ -6806,4 +7115,147 @@ public class ECMA48 implements Runnable {
 
     }
 
+    /**
+     * Parse a "Jexer" image string into a bitmap image, and overlay that
+     * image onto the text cells.
+     *
+     * @param pw width token
+     * @param ph height token
+     * @param ps scroll token
+     * @param data pixel data
+     */
+    private void parseJexerImage(final String pw, final String ph,
+        final String ps, final String data) {
+
+        int imageWidth = 0;
+        int imageHeight = 0;
+        boolean scroll = false;
+        try {
+            imageWidth = Integer.parseInt(pw);
+            imageHeight = Integer.parseInt(ph);
+        } catch (NumberFormatException e) {
+            // SQUASH
+            return;
+        }
+        if ((imageWidth < 1)
+            || (imageWidth > 10000)
+            || (imageHeight < 1)
+            || (imageHeight > 10000)
+        ) {
+            return;
+        }
+        if (ps.equals("1")) {
+            scroll = true;
+        } else if (ps.equals("0")) {
+            scroll = false;
+        } else {
+            return;
+        }
+
+        java.util.Base64.Decoder base64 = java.util.Base64.getDecoder();
+        byte [] bytes = base64.decode(data);
+        if (bytes.length != (imageWidth * imageHeight * 3)) {
+            return;
+        }
+
+        BufferedImage image = new BufferedImage(imageWidth, imageHeight,
+            BufferedImage.TYPE_INT_ARGB);
+
+        for (int x = 0; x < imageWidth; x++) {
+            for (int y = 0; y < imageHeight; y++) {
+                int red   = bytes[(y * imageWidth * 3) + (x * 3)    ];
+                if (red < 0) {
+                    red += 256;
+                }
+                int green = bytes[(y * imageWidth * 3) + (x * 3) + 1];
+                if (green < 0) {
+                    green += 256;
+                }
+                int blue  = bytes[(y * imageWidth * 3) + (x * 3) + 2];
+                if (blue < 0) {
+                    blue += 256;
+                }
+                int rgb = 0xFF000000 | (red << 16) | (green << 8) | blue;
+                image.setRGB(x, y, rgb);
+            }
+        }
+
+        /*
+         * Procedure:
+         *
+         * Break up the image into text cell sized pieces as a new array of
+         * Cells.
+         *
+         * Note original column position x0.
+         *
+         * For each cell:
+         *
+         * 1. Advance (printCharacter(' ')) for horizontal increment, or
+         *    index (linefeed() + cursorPosition(y, x0)) for vertical
+         *    increment.
+         *
+         * 2. Set (x, y) cell image data.
+         *
+         * 3. For the right and bottom edges:
+         *
+         *   a. Render the text to pixels using Terminus font.
+         *
+         *   b. Blit the image on top of the text, using alpha channel.
+         */
+        int cellColumns = image.getWidth() / textWidth;
+        if (cellColumns * textWidth < image.getWidth()) {
+            cellColumns++;
+        }
+        int cellRows = image.getHeight() / textHeight;
+        if (cellRows * textHeight < image.getHeight()) {
+            cellRows++;
+        }
+
+        // Break the image up into an array of cells.
+        Cell [][] cells = new Cell[cellColumns][cellRows];
+
+        for (int x = 0; x < cellColumns; x++) {
+            for (int y = 0; y < cellRows; y++) {
+
+                int width = textWidth;
+                if ((x + 1) * textWidth > image.getWidth()) {
+                    width = image.getWidth() - (x * textWidth);
+                }
+                int height = textHeight;
+                if ((y + 1) * textHeight > image.getHeight()) {
+                    height = image.getHeight() - (y * textHeight);
+                }
+
+                Cell cell = new Cell();
+                cell.setImage(image.getSubimage(x * textWidth,
+                        y * textHeight, width, height));
+
+                cells[x][y] = cell;
+            }
+        }
+
+        int x0 = currentState.cursorX;
+        for (int y = 0; y < cellRows; y++) {
+            for (int x = 0; x < cellColumns; x++) {
+                assert (currentState.cursorX <= rightMargin);
+                DisplayLine line = display.get(currentState.cursorY);
+                line.replace(currentState.cursorX, cells[x][y]);
+                // If at the end of the visible screen, stop.
+                if (currentState.cursorX == rightMargin) {
+                    break;
+                }
+                // Room for more image on the visible screen.
+                currentState.cursorX++;
+            }
+            if ((scroll == true)
+                || ((scroll == false)
+                    && (currentState.cursorY < scrollRegionBottom))
+            ) {
+                linefeed();
+            }
+            cursorPosition(currentState.cursorY, x0);
+        }
+
+    }
+
 }