+ for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+ int rawColor = rawRgbList.get(i);
+ rgbSortedIndex[i] = rgbColorIndices.get(rawColor);
+ }
+ if (DEBUG) {
+ for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+ assert (rawRgbList != null);
+ int idx = rgbSortedIndex[i];
+ int rgbColor = rgbColors.get(idx);
+ if ((idx != 0) && (idx != MAX_COLOR_REGISTERS - 1)) {
+ /*
+ System.err.printf("%d %06x --> %d %06x\n",
+ i, rawRgbList.get(i), idx, rgbColors.get(idx));
+ */
+ assert (rgbColor == rawRgbList.get(i));
+ }
+ }
+ }
+
+ // Set the dimmest color as true black, and the brightest as true
+ // white.
+ rgbColors.set(0, 0);
+ rgbColors.set(MAX_COLOR_REGISTERS - 1, 0xFFFFFF);
+
+ /*
+ System.err.printf("<html><body>\n");
+ for (Integer rgb: rgbColors) {
+ System.err.printf("<font style = \"color:");
+ System.err.printf("#%06x", rgb);
+ System.err.printf(";\">=</font>\n");
+ }
+ System.err.printf("\n</body></html>\n");
+ */
+
+ }
+
+ /**
+ * Emit the sixel palette.
+ *
+ * @param sb the StringBuilder to append to
+ * @param used array of booleans set to true for each color actually
+ * used in this cell, or null to emit the entire palette
+ * @return the string to emit to an ANSI / ECMA-style terminal
+ */
+ public String emitPalette(final StringBuilder sb,
+ final boolean [] used) {
+
+ for (int i = 0; i < MAX_COLOR_REGISTERS; i++) {
+ if (((used != null) && (used[i] == true)) || (used == null)) {
+ int rgbColor = rgbColors.get(i);
+ sb.append(String.format("#%d;2;%d;%d;%d", i,
+ ((rgbColor >>> 16) & 0xFF) * 100 / 255,
+ ((rgbColor >>> 8) & 0xFF) * 100 / 255,
+ ( rgbColor & 0xFF) * 100 / 255));
+ }
+ }
+ return sb.toString();
+ }
+ }
+
+ /**
+ * SixelCache is a least-recently-used cache that hangs on to the
+ * post-rendered sixel string for a particular set of cells.
+ */
+ private class SixelCache {
+
+ /**
+ * Maximum size of the cache.
+ */
+ private int maxSize = 100;
+
+ /**
+ * The entries stored in the cache.
+ */
+ private HashMap<String, CacheEntry> cache = null;
+
+ /**
+ * CacheEntry is one entry in the cache.
+ */
+ private class CacheEntry {
+ /**
+ * The cache key.
+ */
+ public String key;
+
+ /**
+ * The cache data.
+ */
+ public String data;
+
+ /**
+ * The last time this entry was used.
+ */
+ public long millis = 0;
+
+ /**
+ * Public constructor.
+ *
+ * @param key the cache entry key
+ * @param data the cache entry data
+ */
+ public CacheEntry(final String key, final String data) {
+ this.key = key;
+ this.data = data;
+ this.millis = System.currentTimeMillis();
+ }
+ }
+
+ /**
+ * Public constructor.
+ *
+ * @param maxSize the maximum size of the cache
+ */
+ public SixelCache(final int maxSize) {
+ this.maxSize = maxSize;
+ cache = new HashMap<String, CacheEntry>();
+ }
+
+ /**
+ * Make a unique key for a list of cells.
+ *
+ * @param cells the cells
+ * @return the key
+ */
+ private String makeKey(final ArrayList<Cell> cells) {
+ StringBuilder sb = new StringBuilder();
+ for (Cell cell: cells) {
+ sb.append(cell.hashCode());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Get an entry from the cache.
+ *
+ * @param cells the list of cells that are the cache key
+ * @return the sixel string representing these cells, or null if this
+ * list of cells is not in the cache
+ */
+ public String get(final ArrayList<Cell> cells) {
+ CacheEntry entry = cache.get(makeKey(cells));
+ if (entry == null) {
+ return null;
+ }
+ entry.millis = System.currentTimeMillis();
+ return entry.data;
+ }
+
+ /**
+ * Put an entry into the cache.
+ *
+ * @param cells the list of cells that are the cache key
+ * @param data the sixel string representing these cells
+ */
+ public void put(final ArrayList<Cell> cells, final String data) {
+ String key = makeKey(cells);
+
+ // System.err.println("put() " + key + " size " + cache.size());
+
+ assert (!cache.containsKey(key));
+
+ assert (cache.size() <= maxSize);
+ if (cache.size() == maxSize) {
+ // Cache is at limit, evict oldest entry.
+ long oldestTime = Long.MAX_VALUE;
+ String keyToRemove = null;
+ for (CacheEntry entry: cache.values()) {
+ if ((entry.millis < oldestTime) || (keyToRemove == null)) {
+ keyToRemove = entry.key;
+ oldestTime = entry.millis;
+ }
+ }
+ /*
+ System.err.println("put() remove key = " + keyToRemove +
+ " size " + cache.size());
+ */
+ assert (keyToRemove != null);
+ cache.remove(keyToRemove);
+ /*
+ System.err.println("put() removed, size " + cache.size());
+ */
+ }
+ assert (cache.size() <= maxSize);
+ CacheEntry entry = new CacheEntry(key, data);
+ assert (key.equals(entry.key));
+ cache.put(key, entry);
+ /*
+ System.err.println("put() added key " + key + " " +
+ " size " + cache.size());
+ */
+ }
+
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Constructor sets up state for getEvent(). If either windowWidth or
+ * windowHeight are less than 1, the terminal is not resized.
+ *
+ * @param listener the object this backend needs to wake up when new
+ * input comes in
+ * @param input an InputStream connected to the remote user, or null for
+ * System.in. If System.in is used, then on non-Windows systems it will
+ * be put in raw mode; shutdown() will (blindly!) put System.in in cooked
+ * mode. input is always converted to a Reader with UTF-8 encoding.
+ * @param output an OutputStream connected to the remote user, or null
+ * for System.out. output is always converted to a Writer with UTF-8
+ * encoding.
+ * @param windowWidth the number of text columns to start with
+ * @param windowHeight the number of text rows to start with
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public ECMA48Terminal(final Object listener, final InputStream input,
+ final OutputStream output, final int windowWidth,
+ final int windowHeight) throws UnsupportedEncodingException {
+
+ this(listener, input, output);
+
+ // Send dtterm/xterm sequences, which will probably not work because
+ // allowWindowOps is defaulted to false.
+ if ((windowWidth > 0) && (windowHeight > 0)) {
+ String resizeString = String.format("\033[8;%d;%dt", windowHeight,
+ windowWidth);
+ this.output.write(resizeString);
+ this.output.flush();