Merge commit 'e6bb1700749980e69b5e913acbfd276f129c24dc'
[nikiroo-utils.git] / src / jexer / tterminal / ECMA48.java
index c277795d69450b2db44f79d77a1caa8062c11275..537b2e0a4a3ba25238ee5d935f393aa07fcba24c 100644 (file)
  */
 package jexer.tterminal;
 
-import java.awt.Graphics2D;
+import java.awt.Graphics;
 import java.awt.image.BufferedImage;
+import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
 import java.io.CharArrayWriter;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -45,6 +47,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import javax.imageio.ImageIO;
 
 import jexer.TKeypress;
 import jexer.backend.GlyphMaker;
@@ -256,7 +259,7 @@ public class ECMA48 implements Runnable {
     /**
      * The type of emulator to be.
      */
-    private DeviceType type = DeviceType.VT102;
+    private final DeviceType type;
 
     /**
      * The scrollback buffer characters + attributes.
@@ -271,7 +274,7 @@ public class ECMA48 implements Runnable {
     /**
      * The maximum number of lines in the scrollback buffer.
      */
-    private int maxScrollback = 10000;
+    private int scrollbackMax = 10000;
 
     /**
      * The terminal's input.  For type == XTERM, this is an InputStreamReader
@@ -323,29 +326,29 @@ public class ECMA48 implements Runnable {
      * Physical display width.  We start at 80x24, but the user can resize us
      * bigger/smaller.
      */
-    private int width;
+    private int width = 80;
 
     /**
      * Physical display height.  We start at 80x24, but the user can resize
      * us bigger/smaller.
      */
-    private int height;
+    private int height = 24;
 
     /**
      * Top margin of the scrolling region.
      */
-    private int scrollRegionTop;
+    private int scrollRegionTop = 0;
 
     /**
      * Bottom margin of the scrolling region.
      */
-    private int scrollRegionBottom;
+    private int scrollRegionBottom = height - 1;
 
     /**
      * Right margin column number.  This can be selected by the remote side
      * to be 80/132 (rightMargin values 79/131), or it can be (width - 1).
      */
-    private int rightMargin;
+    private int rightMargin = 79;
 
     /**
      * Last character printed.
@@ -357,7 +360,7 @@ public class ECMA48 implements Runnable {
      * 132), but the line does NOT wrap until another character is written to
      * column 1 of the next line, after which the cursor moves to column 2.
      */
-    private boolean wrapLineFlag;
+    private boolean wrapLineFlag = false;
 
     /**
      * VT220 single shift flag.
@@ -394,7 +397,7 @@ public class ECMA48 implements Runnable {
     /**
      * Non-csi collect buffer.
      */
-    private StringBuilder collectBuffer;
+    private StringBuilder collectBuffer = new StringBuilder(128);
 
     /**
      * When true, use the G1 character set.
@@ -469,7 +472,12 @@ public class ECMA48 implements Runnable {
     /**
      * Sixel collection buffer.
      */
-    private StringBuilder sixelParseBuffer;
+    private StringBuilder sixelParseBuffer = new StringBuilder(2048);
+
+    /**
+     * Sixel shared palette.
+     */
+    private HashMap<Integer, java.awt.Color> sixelPalette;
 
     /**
      * The width of a character cell in pixels.
@@ -498,6 +506,11 @@ public class ECMA48 implements Runnable {
      */
     private ArrayList<TInputEvent> userQueue = new ArrayList<TInputEvent>();
 
+    /**
+     * Number of bytes/characters passed to consume().
+     */
+    private long readCount = 0;
+
     /**
      * DECSC/DECRC save/restore a subset of the total state.  This class
      * encapsulates those specific flags/modes.
@@ -650,7 +663,8 @@ public class ECMA48 implements Runnable {
             this.inputStream  = new TimeoutInputStream(inputStream, 2000);
         }
         if (type == DeviceType.XTERM) {
-            this.input    = new InputStreamReader(this.inputStream, "UTF-8");
+            this.input    = new InputStreamReader(new BufferedInputStream(
+                this.inputStream, 1024 * 128), "UTF-8");
             this.output   = new OutputStreamWriter(new
                 BufferedOutputStream(outputStream), "UTF-8");
             this.outputStream = null;
@@ -664,6 +678,8 @@ public class ECMA48 implements Runnable {
         for (int i = 0; i < height; i++) {
             display.add(new DisplayLine(currentState.attr));
         }
+        assert (currentState.cursorY < height);
+        assert (currentState.cursorX < width);
 
         // Spin up the input reader
         readerThread = new Thread(this);
@@ -755,11 +771,28 @@ public class ECMA48 implements Runnable {
                                 int ch = Character.codePointAt(readBufferUTF8,
                                     i);
                                 i += Character.charCount(ch);
-                                consume(ch);
+
+                                // Special case for VT10x: 7-bit characters
+                                // only.
+                                if ((type == DeviceType.VT100)
+                                    || (type == DeviceType.VT102)
+                                ) {
+                                    consume(ch & 0x7F);
+                                } else {
+                                    consume(ch);
+                                }
                             }
                         } else {
                             for (int i = 0; i < rc; i++) {
-                                consume(readBuffer[i]);
+                                // Special case for VT10x: 7-bit characters
+                                // only.
+                                if ((type == DeviceType.VT100)
+                                    || (type == DeviceType.VT102)
+                                ) {
+                                    consume(readBuffer[i] & 0x7F);
+                                } else {
+                                    consume(readBuffer[i]);
+                                }
                             }
                         }
                     }
@@ -826,6 +859,34 @@ public class ECMA48 implements Runnable {
     // ECMA48 -----------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Wait for a period of time to get output from the launched process.
+     *
+     * @param millis millis to wait for, or 0 to wait forever
+     * @return true if the launched process has emitted something
+     */
+    public boolean waitForOutput(final int millis) {
+        if (millis < 0) {
+            throw new IllegalArgumentException("timeout must be >= 0");
+        }
+        int waitedMillis = millis;
+        final int pollTimeout = 5;
+        while (true) {
+            if (readCount != 0) {
+                return true;
+            }
+            if ((millis > 0) && (waitedMillis < 0)){
+                return false;
+            }
+            try {
+                Thread.sleep(pollTimeout);
+            } catch (InterruptedException e) {
+                // SQUASH
+            }
+            waitedMillis -= pollTimeout;
+        }
+    }
+
     /**
      * Process keyboard and mouse events from the user.
      *
@@ -869,12 +930,14 @@ public class ECMA48 implements Runnable {
 
         case VT220:
         case XTERM:
-            // "I am a VT220" - 7 bit version
+            // "I am a VT220" - 7 bit version, with sixel and Jexer image
+            // support.
             if (!s8c1t) {
-                return "\033[?62;1;6;9;4c";
+                return "\033[?62;1;6;9;4;22;444c";
             }
-            // "I am a VT220" - 8 bit version
-            return "\u009b?62;1;6;9;4c";
+            // "I am a VT220" - 8 bit version, with sixel and Jexer image
+            // support.
+            return "\u009b?62;1;6;9;4;22;444c";
         default:
             throw new IllegalArgumentException("Invalid device type: " + type);
         }
@@ -995,11 +1058,6 @@ public class ECMA48 implements Runnable {
         // the input streams.
         if (stopReaderThread == false) {
             stopReaderThread = true;
-            try {
-                readerThread.join(1000);
-            } catch (InterruptedException e) {
-                // SQUASH
-            }
         }
 
         // Now close the output stream.
@@ -1078,6 +1136,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.
      *
@@ -1121,8 +1237,8 @@ public class ECMA48 implements Runnable {
         int delta = height - this.height;
         this.height = height;
         scrollRegionBottom += delta;
-        if (scrollRegionBottom < 0) {
-            scrollRegionBottom = height;
+        if ((scrollRegionBottom < 0) || (scrollRegionTop > height - 1)) {
+            scrollRegionBottom = height - 1;
         }
         if (scrollRegionTop >= scrollRegionBottom) {
             scrollRegionTop = 0;
@@ -1139,10 +1255,29 @@ public class ECMA48 implements Runnable {
             display.add(line);
         }
         while (display.size() > height) {
-            scrollback.add(display.remove(0));
+            appendScrollbackLine(display.remove(0));
         }
     }
 
+    /**
+     * Get the maximum number of lines in the scrollback buffer.
+     *
+     * @return the maximum number of lines in the scrollback buffer
+     */
+    public int getScrollbackMax() {
+        return scrollbackMax;
+    }
+
+    /**
+     * Set the maximum number of lines for the scrollback buffer.
+     *
+     * @param scrollbackMax the maximum number of lines for the scrollback
+     * buffer
+     */
+    public final void setScrollbackMax(final int scrollbackMax) {
+        this.scrollbackMax = scrollbackMax;
+    }
+
     /**
      * Get visible cursor flag.
      *
@@ -1177,7 +1312,7 @@ public class ECMA48 implements Runnable {
      */
     private void toGround() {
         csiParams.clear();
-        collectBuffer = new StringBuilder(8);
+        collectBuffer.setLength(0);
         scanState = ScanState.GROUND;
     }
 
@@ -1200,7 +1335,7 @@ public class ECMA48 implements Runnable {
             colors88.add(0);
         }
 
-        // Set default system colors.
+        // Set default system colors.  These match DOS colors.
         colors88.set(0, 0x00000000);
         colors88.set(1, 0x00a80000);
         colors88.set(2, 0x0000a800);
@@ -1218,6 +1353,249 @@ public class ECMA48 implements Runnable {
         colors88.set(13, 0x00fc54fc);
         colors88.set(14, 0x0054fcfc);
         colors88.set(15, 0x00fcfcfc);
+
+        // These match xterm's default colors from 256colres.h.
+        colors88.set(16, 0x000000);
+        colors88.set(17, 0x00005f);
+        colors88.set(18, 0x000087);
+        colors88.set(19, 0x0000af);
+        colors88.set(20, 0x0000d7);
+        colors88.set(21, 0x0000ff);
+        colors88.set(22, 0x005f00);
+        colors88.set(23, 0x005f5f);
+        colors88.set(24, 0x005f87);
+        colors88.set(25, 0x005faf);
+        colors88.set(26, 0x005fd7);
+        colors88.set(27, 0x005fff);
+        colors88.set(28, 0x008700);
+        colors88.set(29, 0x00875f);
+        colors88.set(30, 0x008787);
+        colors88.set(31, 0x0087af);
+        colors88.set(32, 0x0087d7);
+        colors88.set(33, 0x0087ff);
+        colors88.set(34, 0x00af00);
+        colors88.set(35, 0x00af5f);
+        colors88.set(36, 0x00af87);
+        colors88.set(37, 0x00afaf);
+        colors88.set(38, 0x00afd7);
+        colors88.set(39, 0x00afff);
+        colors88.set(40, 0x00d700);
+        colors88.set(41, 0x00d75f);
+        colors88.set(42, 0x00d787);
+        colors88.set(43, 0x00d7af);
+        colors88.set(44, 0x00d7d7);
+        colors88.set(45, 0x00d7ff);
+        colors88.set(46, 0x00ff00);
+        colors88.set(47, 0x00ff5f);
+        colors88.set(48, 0x00ff87);
+        colors88.set(49, 0x00ffaf);
+        colors88.set(50, 0x00ffd7);
+        colors88.set(51, 0x00ffff);
+        colors88.set(52, 0x5f0000);
+        colors88.set(53, 0x5f005f);
+        colors88.set(54, 0x5f0087);
+        colors88.set(55, 0x5f00af);
+        colors88.set(56, 0x5f00d7);
+        colors88.set(57, 0x5f00ff);
+        colors88.set(58, 0x5f5f00);
+        colors88.set(59, 0x5f5f5f);
+        colors88.set(60, 0x5f5f87);
+        colors88.set(61, 0x5f5faf);
+        colors88.set(62, 0x5f5fd7);
+        colors88.set(63, 0x5f5fff);
+        colors88.set(64, 0x5f8700);
+        colors88.set(65, 0x5f875f);
+        colors88.set(66, 0x5f8787);
+        colors88.set(67, 0x5f87af);
+        colors88.set(68, 0x5f87d7);
+        colors88.set(69, 0x5f87ff);
+        colors88.set(70, 0x5faf00);
+        colors88.set(71, 0x5faf5f);
+        colors88.set(72, 0x5faf87);
+        colors88.set(73, 0x5fafaf);
+        colors88.set(74, 0x5fafd7);
+        colors88.set(75, 0x5fafff);
+        colors88.set(76, 0x5fd700);
+        colors88.set(77, 0x5fd75f);
+        colors88.set(78, 0x5fd787);
+        colors88.set(79, 0x5fd7af);
+        colors88.set(80, 0x5fd7d7);
+        colors88.set(81, 0x5fd7ff);
+        colors88.set(82, 0x5fff00);
+        colors88.set(83, 0x5fff5f);
+        colors88.set(84, 0x5fff87);
+        colors88.set(85, 0x5fffaf);
+        colors88.set(86, 0x5fffd7);
+        colors88.set(87, 0x5fffff);
+        colors88.set(88, 0x870000);
+        colors88.set(89, 0x87005f);
+        colors88.set(90, 0x870087);
+        colors88.set(91, 0x8700af);
+        colors88.set(92, 0x8700d7);
+        colors88.set(93, 0x8700ff);
+        colors88.set(94, 0x875f00);
+        colors88.set(95, 0x875f5f);
+        colors88.set(96, 0x875f87);
+        colors88.set(97, 0x875faf);
+        colors88.set(98, 0x875fd7);
+        colors88.set(99, 0x875fff);
+        colors88.set(100, 0x878700);
+        colors88.set(101, 0x87875f);
+        colors88.set(102, 0x878787);
+        colors88.set(103, 0x8787af);
+        colors88.set(104, 0x8787d7);
+        colors88.set(105, 0x8787ff);
+        colors88.set(106, 0x87af00);
+        colors88.set(107, 0x87af5f);
+        colors88.set(108, 0x87af87);
+        colors88.set(109, 0x87afaf);
+        colors88.set(110, 0x87afd7);
+        colors88.set(111, 0x87afff);
+        colors88.set(112, 0x87d700);
+        colors88.set(113, 0x87d75f);
+        colors88.set(114, 0x87d787);
+        colors88.set(115, 0x87d7af);
+        colors88.set(116, 0x87d7d7);
+        colors88.set(117, 0x87d7ff);
+        colors88.set(118, 0x87ff00);
+        colors88.set(119, 0x87ff5f);
+        colors88.set(120, 0x87ff87);
+        colors88.set(121, 0x87ffaf);
+        colors88.set(122, 0x87ffd7);
+        colors88.set(123, 0x87ffff);
+        colors88.set(124, 0xaf0000);
+        colors88.set(125, 0xaf005f);
+        colors88.set(126, 0xaf0087);
+        colors88.set(127, 0xaf00af);
+        colors88.set(128, 0xaf00d7);
+        colors88.set(129, 0xaf00ff);
+        colors88.set(130, 0xaf5f00);
+        colors88.set(131, 0xaf5f5f);
+        colors88.set(132, 0xaf5f87);
+        colors88.set(133, 0xaf5faf);
+        colors88.set(134, 0xaf5fd7);
+        colors88.set(135, 0xaf5fff);
+        colors88.set(136, 0xaf8700);
+        colors88.set(137, 0xaf875f);
+        colors88.set(138, 0xaf8787);
+        colors88.set(139, 0xaf87af);
+        colors88.set(140, 0xaf87d7);
+        colors88.set(141, 0xaf87ff);
+        colors88.set(142, 0xafaf00);
+        colors88.set(143, 0xafaf5f);
+        colors88.set(144, 0xafaf87);
+        colors88.set(145, 0xafafaf);
+        colors88.set(146, 0xafafd7);
+        colors88.set(147, 0xafafff);
+        colors88.set(148, 0xafd700);
+        colors88.set(149, 0xafd75f);
+        colors88.set(150, 0xafd787);
+        colors88.set(151, 0xafd7af);
+        colors88.set(152, 0xafd7d7);
+        colors88.set(153, 0xafd7ff);
+        colors88.set(154, 0xafff00);
+        colors88.set(155, 0xafff5f);
+        colors88.set(156, 0xafff87);
+        colors88.set(157, 0xafffaf);
+        colors88.set(158, 0xafffd7);
+        colors88.set(159, 0xafffff);
+        colors88.set(160, 0xd70000);
+        colors88.set(161, 0xd7005f);
+        colors88.set(162, 0xd70087);
+        colors88.set(163, 0xd700af);
+        colors88.set(164, 0xd700d7);
+        colors88.set(165, 0xd700ff);
+        colors88.set(166, 0xd75f00);
+        colors88.set(167, 0xd75f5f);
+        colors88.set(168, 0xd75f87);
+        colors88.set(169, 0xd75faf);
+        colors88.set(170, 0xd75fd7);
+        colors88.set(171, 0xd75fff);
+        colors88.set(172, 0xd78700);
+        colors88.set(173, 0xd7875f);
+        colors88.set(174, 0xd78787);
+        colors88.set(175, 0xd787af);
+        colors88.set(176, 0xd787d7);
+        colors88.set(177, 0xd787ff);
+        colors88.set(178, 0xd7af00);
+        colors88.set(179, 0xd7af5f);
+        colors88.set(180, 0xd7af87);
+        colors88.set(181, 0xd7afaf);
+        colors88.set(182, 0xd7afd7);
+        colors88.set(183, 0xd7afff);
+        colors88.set(184, 0xd7d700);
+        colors88.set(185, 0xd7d75f);
+        colors88.set(186, 0xd7d787);
+        colors88.set(187, 0xd7d7af);
+        colors88.set(188, 0xd7d7d7);
+        colors88.set(189, 0xd7d7ff);
+        colors88.set(190, 0xd7ff00);
+        colors88.set(191, 0xd7ff5f);
+        colors88.set(192, 0xd7ff87);
+        colors88.set(193, 0xd7ffaf);
+        colors88.set(194, 0xd7ffd7);
+        colors88.set(195, 0xd7ffff);
+        colors88.set(196, 0xff0000);
+        colors88.set(197, 0xff005f);
+        colors88.set(198, 0xff0087);
+        colors88.set(199, 0xff00af);
+        colors88.set(200, 0xff00d7);
+        colors88.set(201, 0xff00ff);
+        colors88.set(202, 0xff5f00);
+        colors88.set(203, 0xff5f5f);
+        colors88.set(204, 0xff5f87);
+        colors88.set(205, 0xff5faf);
+        colors88.set(206, 0xff5fd7);
+        colors88.set(207, 0xff5fff);
+        colors88.set(208, 0xff8700);
+        colors88.set(209, 0xff875f);
+        colors88.set(210, 0xff8787);
+        colors88.set(211, 0xff87af);
+        colors88.set(212, 0xff87d7);
+        colors88.set(213, 0xff87ff);
+        colors88.set(214, 0xffaf00);
+        colors88.set(215, 0xffaf5f);
+        colors88.set(216, 0xffaf87);
+        colors88.set(217, 0xffafaf);
+        colors88.set(218, 0xffafd7);
+        colors88.set(219, 0xffafff);
+        colors88.set(220, 0xffd700);
+        colors88.set(221, 0xffd75f);
+        colors88.set(222, 0xffd787);
+        colors88.set(223, 0xffd7af);
+        colors88.set(224, 0xffd7d7);
+        colors88.set(225, 0xffd7ff);
+        colors88.set(226, 0xffff00);
+        colors88.set(227, 0xffff5f);
+        colors88.set(228, 0xffff87);
+        colors88.set(229, 0xffffaf);
+        colors88.set(230, 0xffffd7);
+        colors88.set(231, 0xffffff);
+        colors88.set(232, 0x080808);
+        colors88.set(233, 0x121212);
+        colors88.set(234, 0x1c1c1c);
+        colors88.set(235, 0x262626);
+        colors88.set(236, 0x303030);
+        colors88.set(237, 0x3a3a3a);
+        colors88.set(238, 0x444444);
+        colors88.set(239, 0x4e4e4e);
+        colors88.set(240, 0x585858);
+        colors88.set(241, 0x626262);
+        colors88.set(242, 0x6c6c6c);
+        colors88.set(243, 0x767676);
+        colors88.set(244, 0x808080);
+        colors88.set(245, 0x8a8a8a);
+        colors88.set(246, 0x949494);
+        colors88.set(247, 0x9e9e9e);
+        colors88.set(248, 0xa8a8a8);
+        colors88.set(249, 0xb2b2b2);
+        colors88.set(250, 0xbcbcbc);
+        colors88.set(251, 0xc6c6c6);
+        colors88.set(252, 0xd0d0d0);
+        colors88.set(253, 0xdadada);
+        colors88.set(254, 0xe4e4e4);
+        colors88.set(255, 0xeeeeee);
+
     }
 
     /**
@@ -1292,8 +1670,13 @@ public class ECMA48 implements Runnable {
         currentState            = new SaveableState();
         savedState              = new SaveableState();
         scanState               = ScanState.GROUND;
-        width                   = 80;
-        height                  = 24;
+        if (displayListener != null) {
+            width = displayListener.getDisplayWidth();
+            height = displayListener.getDisplayHeight();
+        } else {
+            width               = 80;
+            height              = 24;
+        }
         scrollRegionTop         = 0;
         scrollRegionBottom      = height - 1;
         rightMargin             = width - 1;
@@ -1301,11 +1684,6 @@ public class ECMA48 implements Runnable {
         arrowKeyMode            = ArrowKeyMode.ANSI;
         keypadMode              = KeypadMode.Numeric;
         wrapLineFlag            = false;
-        if (displayListener != null) {
-            width = displayListener.getDisplayWidth();
-            height = displayListener.getDisplayHeight();
-            rightMargin         = width - 1;
-        }
 
         // Flags
         shiftOut                = false;
@@ -1336,14 +1714,25 @@ public class ECMA48 implements Runnable {
         toGround();
     }
 
+    /**
+     * Append a to the scrollback buffer, clearing image data for lines more
+     * than three screenfuls in.
+     */
+    private void appendScrollbackLine(DisplayLine line) {
+        scrollback.add(line);
+        if (scrollback.size() > height * 3) {
+            scrollback.get(scrollback.size() - (height * 3)).clearImages();
+        }
+    }
+
     /**
      * Append a new line to the bottom of the display, adding lines off the
      * top to the scrollback buffer.
      */
     private void newDisplayLine() {
         // Scroll the top line off into the scrollback buffer
-        scrollback.add(display.get(0));
-        if (scrollback.size() > maxScrollback) {
+        appendScrollbackLine(display.get(0));
+        while (scrollback.size() > scrollbackMax) {
             scrollback.remove(0);
             scrollback.trimToSize();
         }
@@ -1388,7 +1777,6 @@ public class ECMA48 implements Runnable {
      * Handle a linefeed.
      */
     private void linefeed() {
-
         if (currentState.cursorY < scrollRegionBottom) {
             // Increment screen y
             currentState.cursorY++;
@@ -1599,35 +1987,45 @@ public class ECMA48 implements Runnable {
         if (mouseEncoding == MouseEncoding.SGR) {
             sb.append((char) 0x1B);
             sb.append("[<");
+            int buttons = 0;
 
             if (mouse.isMouse1()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("32;");
+                    buttons = 32;
                 } else {
-                    sb.append("0;");
+                    buttons = 0;
                 }
             } else if (mouse.isMouse2()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("33;");
+                    buttons = 33;
                 } else {
-                    sb.append("1;");
+                    buttons = 1;
                 }
             } else if (mouse.isMouse3()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append("34;");
+                    buttons = 34;
                 } else {
-                    sb.append("2;");
+                    buttons = 2;
                 }
             } else if (mouse.isMouseWheelUp()) {
-                sb.append("64;");
+                buttons = 64;
             } else if (mouse.isMouseWheelDown()) {
-                sb.append("65;");
+                buttons = 65;
             } else {
                 // This is motion with no buttons down.
-                sb.append("35;");
+                buttons = 35;
+            }
+            if (mouse.isAlt()) {
+                buttons |= 0x08;
+            }
+            if (mouse.isCtrl()) {
+                buttons |= 0x10;
+            }
+            if (mouse.isShift()) {
+                buttons |= 0x04;
             }
 
-            sb.append(String.format("%d;%d", mouse.getX() + 1,
+            sb.append(String.format("%d;%d;%d", buttons, mouse.getX() + 1,
                     mouse.getY() + 1));
 
             if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
@@ -1641,35 +2039,46 @@ public class ECMA48 implements Runnable {
             sb.append((char) 0x1B);
             sb.append('[');
             sb.append('M');
+            int buttons = 0;
             if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) {
-                sb.append((char) (0x03 + 32));
+                buttons = 0x03 + 32;
             } else if (mouse.isMouse1()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x00 + 32 + 32));
+                    buttons = 0x00 + 32 + 32;
                 } else {
-                    sb.append((char) (0x00 + 32));
+                    buttons = 0x00 + 32;
                 }
             } else if (mouse.isMouse2()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x01 + 32 + 32));
+                    buttons = 0x01 + 32 + 32;
                 } else {
-                    sb.append((char) (0x01 + 32));
+                    buttons = 0x01 + 32;
                 }
             } else if (mouse.isMouse3()) {
                 if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) {
-                    sb.append((char) (0x02 + 32 + 32));
+                    buttons = 0x02 + 32 + 32;
                 } else {
-                    sb.append((char) (0x02 + 32));
+                    buttons = 0x02 + 32;
                 }
             } else if (mouse.isMouseWheelUp()) {
-                sb.append((char) (0x04 + 64));
+                buttons = 0x04 + 64;
             } else if (mouse.isMouseWheelDown()) {
-                sb.append((char) (0x05 + 64));
+                buttons = 0x05 + 64;
             } else {
                 // This is motion with no buttons down.
-                sb.append((char) (0x03 + 32));
+                buttons = 0x03 + 32;
+            }
+            if (mouse.isAlt()) {
+                buttons |= 0x08;
+            }
+            if (mouse.isCtrl()) {
+                buttons |= 0x10;
+            }
+            if (mouse.isShift()) {
+                buttons |= 0x04;
             }
 
+            sb.append((char) (buttons & 0xFF));
             sb.append((char) (mouse.getX() + 33));
             sb.append((char) (mouse.getY() + 33));
         }
@@ -1748,6 +2157,9 @@ public class ECMA48 implements Runnable {
                 // Local echo for everything else
                 printCharacter(keypress.getChar());
             }
+            if (displayListener != null) {
+                displayListener.displayChanged();
+            }
         }
 
         if ((newLineMode == true) && (keypress.equals(kbEnter))) {
@@ -2816,6 +3228,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;
@@ -3082,6 +3495,21 @@ public class ECMA48 implements Runnable {
 
                 break;
 
+            case 80:
+                if (type == DeviceType.XTERM) {
+                    if (decPrivateModeFlag == true) {
+                        if (value == true) {
+                            // Enable sixel scrolling (default).
+                            // Not supported
+                        } else {
+                            // Disable sixel scrolling.
+                            // Not supported
+                        }
+                    }
+                }
+
+                break;
+
             case 1000:
                 if ((type == DeviceType.XTERM)
                     && (decPrivateModeFlag == true)
@@ -3147,6 +3575,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;
 
@@ -3839,14 +4283,14 @@ public class ECMA48 implements Runnable {
                      * RGB color mode.
                      */
                     rgbColor = true;
-                    break;
+                    continue;
 
                 case 5:
                     /*
                      * Indexed color mode.
                      */
                     idx88Color = true;
-                    break;
+                    continue;
 
                 default:
                     /*
@@ -3894,7 +4338,7 @@ public class ECMA48 implements Runnable {
 
                 case 8:
                     // Invisible
-                    // TODO
+                    // Not supported
                     break;
 
                 case 90:
@@ -4279,6 +4723,9 @@ public class ECMA48 implements Runnable {
             // DECSTBM
             int top = getCsiParam(0, 1, 1, height) - 1;
             int bottom = getCsiParam(1, height, 1, height) - 1;
+            if (bottom > height - 1) {
+                bottom = height - 1;
+            }
 
             if (top > bottom) {
                 top = bottom;
@@ -4632,13 +5079,22 @@ public class ECMA48 implements Runnable {
     private void oscPut(final char xtermChar) {
         // System.err.println("oscPut: " + xtermChar);
 
+        boolean oscEnd = false;
+
+        if (xtermChar == 0x07) {
+            oscEnd = true;
+        }
+        if ((xtermChar == '\\')
+            && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033')
+        ) {
+            oscEnd = true;
+        }
+
         // Collect first
         collectBuffer.append(xtermChar);
 
         // Xterm cases...
-        if ((xtermChar == 0x07)
-            || (collectBuffer.toString().endsWith("\033\\"))
-        ) {
+        if (oscEnd) {
             String args = null;
             if (xtermChar == 0x07) {
                 args = collectBuffer.substring(0, collectBuffer.length() - 1);
@@ -4665,6 +5121,45 @@ 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")) {
+                    if (p[1].equals("0") && (p.length == 6)) {
+                        // Jexer image - RGB
+                        parseJexerImageRGB(p[2], p[3], p[4], p[5]);
+                    } else if (p[1].equals("1") && (p.length == 4)) {
+                        // Jexer image - PNG
+                        parseJexerImageFile(1, p[2], p[3]);
+                    } else if (p[1].equals("2") && (p.length == 4)) {
+                        // Jexer image - JPG
+                        parseJexerImageFile(2, p[2], p[3]);
+                    }
+                }
             }
 
             // Go to SCAN_GROUND state
@@ -4682,11 +5177,19 @@ public class ECMA48 implements Runnable {
     private void pmPut(final char pmChar) {
         // System.err.println("pmPut: " + pmChar);
 
+        boolean pmEnd = false;
+
+        if ((pmChar == '\\')
+            && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033')
+        ) {
+            pmEnd = true;
+        }
+
         // Collect first
         collectBuffer.append(pmChar);
 
         // Xterm cases...
-        if (collectBuffer.toString().endsWith("\033\\")) {
+        if (pmEnd) {
             String arg = null;
             arg = collectBuffer.substring(0, collectBuffer.length() - 2);
 
@@ -4745,21 +5248,40 @@ public class ECMA48 implements Runnable {
         }
     }
 
+    /**
+     * 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(int ch) {
+    private void consume(final int ch) {
+        readCount++;
 
         // 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 = (ch & 0x7F);
-        }
-
         // Special "anywhere" states
 
         // 18, 1A                     --> execute, then switch to SCAN_GROUND
@@ -5908,7 +6430,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':
@@ -6182,7 +6715,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':
@@ -6510,7 +7054,7 @@ public class ECMA48 implements Runnable {
 
             // 0x71 goes to DCS_SIXEL
             if (ch == 0x71) {
-                sixelParseBuffer = new StringBuilder();
+                sixelParseBuffer.setLength(0);
                 scanState = ScanState.DCS_SIXEL;
             } else if ((ch >= 0x40) && (ch <= 0x7E)) {
                 // 0x40-7E goes to DCS_PASSTHROUGH
@@ -6595,7 +7139,7 @@ public class ECMA48 implements Runnable {
 
             // 0x71 goes to DCS_SIXEL
             if (ch == 0x71) {
-                sixelParseBuffer = new StringBuilder();
+                sixelParseBuffer.setLength(0);
                 scanState = ScanState.DCS_SIXEL;
             } else if ((ch >= 0x40) && (ch <= 0x7E)) {
                 // 0x40-7E goes to DCS_PASSTHROUGH
@@ -6622,17 +7166,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;
             }
 
@@ -6655,11 +7202,13 @@ public class ECMA48 implements Runnable {
             if (ch == 0x9C) {
                 parseSixel();
                 toGround();
+                return;
             }
 
             // 0x1B 0x5C goes to GROUND
             if (ch == 0x1B) {
                 collect((char) ch);
+                return;
             }
             if (ch == 0x5C) {
                 if ((collectBuffer.length() > 0)
@@ -6667,29 +7216,20 @@ public class ECMA48 implements Runnable {
                 ) {
                     parseSixel();
                     toGround();
+                    return;
                 }
             }
 
             // 00-17, 19, 1C-1F, 20-7E   --> put
-            if (ch <= 0x17) {
-                sixelParseBuffer.append((char) ch);
-                return;
-            }
-            if (ch == 0x19) {
-                sixelParseBuffer.append((char) ch);
-                return;
-            }
-            if ((ch >= 0x1C) && (ch <= 0x1F)) {
-                sixelParseBuffer.append((char) ch);
-                return;
-            }
-            if ((ch >= 0x20) && (ch <= 0x7E)) {
+            if ((ch <= 0x17)
+                || (ch == 0x19)
+                || ((ch >= 0x1C) && (ch <= 0x1F))
+                || ((ch >= 0x20) && (ch <= 0x7E))
+            ) {
                 sixelParseBuffer.append((char) ch);
-                return;
             }
 
             // 7F                        --> ignore
-
             return;
 
         case SOSPMAPC_STRING:
@@ -6787,9 +7327,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.
@@ -6818,9 +7392,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);
@@ -6829,6 +7403,167 @@ public class ECMA48 implements Runnable {
             // Sixel data was malformed in some way, bail out.
             return;
         }
+        if ((image.getWidth() < 1)
+            || (image.getWidth() > 10000)
+            || (image.getHeight() < 1)
+            || (image.getHeight() > 10000)
+        ) {
+            return;
+        }
+
+        imageToCells(image, true);
+    }
+
+    /**
+     * Parse a "Jexer" RGB 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 parseJexerImageRGB(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;
+        }
+
+        byte [] bytes = StringUtils.fromBase64(data.getBytes());
+        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);
+            }
+        }
+
+        imageToCells(image, scroll);
+    }
+
+    /**
+     * Parse a "Jexer" PNG or JPG image string into a bitmap image, and
+     * overlay that image onto the text cells.
+     *
+     * @param type 1 for PNG, 2 for JPG
+     * @param ps scroll token
+     * @param data pixel data
+     */
+    private void parseJexerImageFile(final int type, final String ps,
+        final String data) {
+
+        int imageWidth = 0;
+        int imageHeight = 0;
+        boolean scroll = false;
+        BufferedImage image = null;
+        try {
+            byte [] bytes = StringUtils.fromBase64(data.getBytes());
+
+            switch (type) {
+            case 1:
+                if ((bytes[0] != (byte) 0x89)
+                    || (bytes[1] != 'P')
+                    || (bytes[2] != 'N')
+                    || (bytes[3] != 'G')
+                    || (bytes[4] != (byte) 0x0D)
+                    || (bytes[5] != (byte) 0x0A)
+                    || (bytes[6] != (byte) 0x1A)
+                    || (bytes[7] != (byte) 0x0A)
+                ) {
+                    // File does not have PNG header, bail out.
+                    return;
+                }
+                break;
+
+            case 2:
+                if ((bytes[0] != (byte) 0XFF)
+                    || (bytes[1] != (byte) 0xD8)
+                    || (bytes[2] != (byte) 0xFF)
+                ) {
+                    // File does not have JPG header, bail out.
+                    return;
+                }
+                break;
+
+            default:
+                // Unsupported type, bail out.
+                return;
+            }
+
+            image = ImageIO.read(new ByteArrayInputStream(bytes));
+        } catch (IOException e) {
+            // SQUASH
+            return;
+        }
+        assert (image != null);
+        imageWidth = image.getWidth();
+        imageHeight = image.getHeight();
+        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;
+        }
+
+        imageToCells(image, scroll);
+    }
+
+    /**
+     * Break up an image into the cells at the current cursor.
+     *
+     * @param image the image to display
+     * @param scroll if true, scroll the image and move the cursor
+     */
+    private void imageToCells(final BufferedImage image, final boolean scroll) {
+        assert (image != null);
 
         /*
          * Procedure:
@@ -6877,72 +7612,64 @@ public class ECMA48 implements Runnable {
                 }
 
                 Cell cell = new Cell();
-                cell.setImage(image.getSubimage(x * textWidth,
-                        y * textHeight, width, height));
+                if ((width != textWidth) || (height != textHeight)) {
+                    BufferedImage newImage;
+                    newImage = new BufferedImage(textWidth, textHeight,
+                        BufferedImage.TYPE_INT_ARGB);
+
+                    Graphics gr = newImage.getGraphics();
+                    gr.drawImage(image.getSubimage(x * textWidth,
+                            y * textHeight, width, height),
+                        0, 0, null, null);
+                    gr.dispose();
+                    cell.setImage(newImage);
+                } else {
+                    cell.setImage(image.getSubimage(x * textWidth,
+                            y * textHeight, width, height));
+                }
 
                 cells[x][y] = cell;
             }
         }
 
         int x0 = currentState.cursorX;
+        int y0 = currentState.cursorY;
         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);
+
+                // A real sixel terminal would render the text of the current
+                // cell first, then image over it (accounting for blank
+                // pixels).  We do not support that.  A cell is either text,
+                // or image, but not a mix of image-over-text.
+                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();
+            if (currentState.cursorY < scrollRegionBottom - 1) {
+                // Not at the bottom, down a line.
+                linefeed();
+            } else if (scroll == true) {
+                // At the bottom, scroll as needed.
+                linefeed();
+            } else {
+                // At the bottom, no more scrolling, done.
+                break;
+            }
+
             cursorPosition(currentState.cursorY, x0);
         }
 
-    }
-
-    /**
-     * 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;
+        if (scroll == false) {
+            cursorPosition(y0, x0);
         }
 
-        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);
     }
 
 }