+ /**
+ * Perform a somewhat-optimal rendering of a line.
+ *
+ * @param y row coordinate. 0 is the top-most row.
+ * @param sb StringBuilder to write escape sequences to
+ * @param lastAttr cell attributes from the last call to flushLine
+ */
+ private void flushLine(final int y, final StringBuilder sb,
+ CellAttributes lastAttr) {
+
+ int lastX = -1;
+ int textEnd = 0;
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ if (!lCell.isBlank()) {
+ textEnd = x;
+ }
+ }
+ // Push textEnd to first column beyond the text area
+ textEnd++;
+
+ // DEBUG
+ // reallyCleared = true;
+
+ for (int x = 0; x < width; x++) {
+ Cell lCell = logical[x][y];
+ Cell pCell = physical[x][y];
+
+ if (!lCell.equals(pCell) || reallyCleared) {
+
+ if (debugToStderr) {
+ System.err.printf("\n--\n");
+ System.err.printf(" Y: %d X: %d\n", y, x);
+ System.err.printf(" lCell: %s\n", lCell);
+ System.err.printf(" pCell: %s\n", pCell);
+ System.err.printf(" ==== \n");
+ }
+
+ if (lastAttr == null) {
+ lastAttr = new CellAttributes();
+ sb.append(normal());
+ }
+
+ // Place the cell
+ if ((lastX != (x - 1)) || (lastX == -1)) {
+ // Advancing at least one cell, or the first gotoXY
+ sb.append(gotoXY(x, y));
+ }
+
+ assert (lastAttr != null);
+
+ if ((x == textEnd) && (textEnd < width - 1)) {
+ assert (lCell.isBlank());
+
+ for (int i = x; i < width; i++) {
+ assert (logical[i][y].isBlank());
+ // Physical is always updated
+ physical[i][y].reset();
+ }
+
+ // Clear remaining line
+ sb.append(clearRemainingLine());
+ lastAttr.reset();
+ return;
+ }
+
+ // Now emit only the modified attributes
+ if ((lCell.getForeColor() != lastAttr.getForeColor())
+ && (lCell.getBackColor() != lastAttr.getBackColor())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+ // Both colors changed, attributes the same
+ sb.append(color(lCell.isBold(),
+ lCell.getForeColor(), lCell.getBackColor()));
+
+ if (debugToStderr) {
+ System.err.printf("1 Change only fore/back colors\n");
+ }
+ } else if ((lCell.getForeColor() != lastAttr.getForeColor())
+ && (lCell.getBackColor() != lastAttr.getBackColor())
+ && (lCell.isBold() != lastAttr.isBold())
+ && (lCell.isReverse() != lastAttr.isReverse())
+ && (lCell.isUnderline() != lastAttr.isUnderline())
+ && (lCell.isBlink() != lastAttr.isBlink())
+ ) {
+ // Everything is different
+ sb.append(color(lCell.getForeColor(),
+ lCell.getBackColor(),
+ lCell.isBold(), lCell.isReverse(),
+ lCell.isBlink(),
+ lCell.isUnderline()));
+
+ if (debugToStderr) {
+ System.err.printf("2 Set all attributes\n");
+ }
+ } else if ((lCell.getForeColor() != lastAttr.getForeColor())
+ && (lCell.getBackColor() == lastAttr.getBackColor())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+
+ // Attributes same, foreColor different
+ sb.append(color(lCell.isBold(),
+ lCell.getForeColor(), true));
+
+ if (debugToStderr) {
+ System.err.printf("3 Change foreColor\n");
+ }
+ } else if ((lCell.getForeColor() == lastAttr.getForeColor())
+ && (lCell.getBackColor() != lastAttr.getBackColor())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+ // Attributes same, backColor different
+ sb.append(color(lCell.isBold(),
+ lCell.getBackColor(), false));
+
+ if (debugToStderr) {
+ System.err.printf("4 Change backColor\n");
+ }
+ } else if ((lCell.getForeColor() == lastAttr.getForeColor())
+ && (lCell.getBackColor() == lastAttr.getBackColor())
+ && (lCell.isBold() == lastAttr.isBold())
+ && (lCell.isReverse() == lastAttr.isReverse())
+ && (lCell.isUnderline() == lastAttr.isUnderline())
+ && (lCell.isBlink() == lastAttr.isBlink())
+ ) {
+
+ // All attributes the same, just print the char
+ // NOP
+
+ if (debugToStderr) {
+ System.err.printf("5 Only emit character\n");
+ }
+ } else {
+ // Just reset everything again
+ sb.append(color(lCell.getForeColor(),
+ lCell.getBackColor(),
+ lCell.isBold(),
+ lCell.isReverse(),
+ lCell.isBlink(),
+ lCell.isUnderline()));
+
+ if (debugToStderr) {
+ System.err.printf("6 Change all attributes\n");
+ }
+ }
+ // Emit the character
+ sb.append(lCell.getChar());
+
+ // Save the last rendered cell
+ lastX = x;
+ lastAttr.setTo(lCell);
+
+ // Physical is always updated
+ physical[x][y].setTo(lCell);
+
+ } // if (!lCell.equals(pCell) || (reallyCleared == true))
+
+ } // for (int x = 0; x < width; x++)
+ }
+
+ /**
+ * Render the screen to a string that can be emitted to something that
+ * knows how to process ECMA-48/ANSI X3.64 escape sequences.
+ *
+ * @return escape sequences string that provides the updates to the
+ * physical screen
+ */
+ private String flushString() {
+ if (!dirty) {
+ assert (!reallyCleared);
+ return "";
+ }
+
+ CellAttributes attr = null;
+
+ StringBuilder sb = new StringBuilder();
+ if (reallyCleared) {
+ attr = new CellAttributes();
+ sb.append(clearAll());
+ }
+
+ for (int y = 0; y < height; y++) {
+ flushLine(y, sb, attr);
+ }
+
+ dirty = false;
+ reallyCleared = false;
+
+ String result = sb.toString();
+ if (debugToStderr) {
+ System.err.printf("flushString(): %s\n", result);
+ }
+ return result;
+ }
+
+ /**
+ * Push the logical screen to the physical device.
+ */
+ @Override
+ public void flushPhysical() {
+ String result = flushString();
+ if ((cursorVisible)
+ && (cursorY <= height - 1)
+ && (cursorX <= width - 1)
+ ) {
+ result += cursor(true);
+ result += gotoXY(cursorX, cursorY);
+ } else {
+ result += cursor(false);
+ }
+ output.write(result);
+ flush();
+ }
+
+ /**
+ * Set the window title.
+ *
+ * @param title the new title
+ */
+ public void setTitle(final String title) {
+ output.write(getSetTitleString(title));
+ flush();
+ }
+