* @author Kevin Lamonte [kevin.lamonte@gmail.com]
* @version 1
*/
-package jexer.io;
+package jexer.backend;
import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.util.List;
import java.util.LinkedList;
+import jexer.bits.Cell;
+import jexer.bits.CellAttributes;
import jexer.bits.Color;
import jexer.event.TInputEvent;
import jexer.event.TKeypressEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
-import jexer.session.SessionInfo;
-import jexer.session.TSessionInfo;
-import jexer.session.TTYSessionInfo;
import static jexer.TKeypress.*;
/**
* This class reads keystrokes and mouse events and emits output to ANSI
* X3.64 / ECMA-48 type terminals e.g. xterm, linux, vt100, ansi.sys, etc.
*/
-public final class ECMA48Terminal implements Runnable {
+public final class ECMA48Terminal extends LogicalScreen
+ implements TerminalReader, Runnable {
+
+ /**
+ * Emit debugging to stderr.
+ */
+ private boolean debugToStderr = false;
/**
* If true, emit T.416-style RGB colors. This is a) expensive in
public ECMA48Terminal(final Object listener, final InputStream input,
final OutputStream output) throws UnsupportedEncodingException {
- reset();
+ resetParser();
mouse1 = false;
mouse2 = false;
mouse3 = false;
eventQueue = new LinkedList<TInputEvent>();
readerThread = new Thread(this);
readerThread.start();
+
+ // Query the screen size
+ setDimensions(sessionInfo.getWindowWidth(),
+ sessionInfo.getWindowHeight());
+
+ // Clear the screen
+ this.output.write(clearAll());
+ this.output.flush();
}
/**
if (writer == null) {
throw new IllegalArgumentException("Writer must be specified");
}
- reset();
+ resetParser();
mouse1 = false;
mouse2 = false;
mouse3 = false;
eventQueue = new LinkedList<TInputEvent>();
readerThread = new Thread(this);
readerThread.start();
+
+ // Query the screen size
+ setDimensions(sessionInfo.getWindowWidth(),
+ sessionInfo.getWindowHeight());
+
+ // Clear the screen
+ this.output.write(clearAll());
+ this.output.flush();
}
/**
/**
* Restore terminal to normal state.
*/
- public void shutdown() {
+ public void closeTerminal() {
// System.err.println("=== shutdown() ==="); System.err.flush();
output.flush();
}
+ /**
+ * 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();
+ }
+
/**
* Reset keyboard/mouse input parser.
*/
- private void reset() {
+ private void resetParser() {
state = ParseState.GROUND;
params = new ArrayList<String>();
params.clear();
if (escDelay > 100) {
// After 0.1 seconds, assume a true escape character
queue.add(controlChar((char)0x1B, false));
- reset();
+ resetParser();
}
}
}
if (escDelay > 250) {
// After 0.25 seconds, assume a true escape character
events.add(controlChar((char)0x1B, false));
- reset();
+ resetParser();
}
}
if (ch <= 0x1F) {
// Control character
events.add(controlChar(ch, false));
- reset();
+ resetParser();
return;
}
// Normal character
events.add(new TKeypressEvent(false, 0, ch,
false, false, false));
- reset();
+ resetParser();
return;
}
if (ch <= 0x1F) {
// ALT-Control character
events.add(controlChar(ch, true));
- reset();
+ resetParser();
return;
}
}
alt = true;
events.add(new TKeypressEvent(false, 0, ch, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case ESCAPE_INTERMEDIATE:
default:
break;
}
- reset();
+ resetParser();
return;
}
// Unknown keystroke, ignore
- reset();
+ resetParser();
return;
case CSI_ENTRY:
case 'A':
// Up
events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'B':
// Down
events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'C':
// Right
events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'D':
// Left
events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'H':
// Home
events.add(new TKeypressEvent(kbHome));
- reset();
+ resetParser();
return;
case 'F':
// End
events.add(new TKeypressEvent(kbEnd));
- reset();
+ resetParser();
return;
case 'Z':
// CBT - Cursor backward X tab stops (default 1)
events.add(new TKeypressEvent(kbBackTab));
- reset();
+ resetParser();
return;
case 'M':
// Mouse position
}
// Unknown keystroke, ignore
- reset();
+ resetParser();
return;
case MOUSE_SGR:
if (event != null) {
events.add(event);
}
- reset();
+ resetParser();
return;
case 'm':
// Generate a mouse release event
if (event != null) {
events.add(event);
}
- reset();
+ resetParser();
return;
default:
break;
}
// Unknown keystroke, ignore
- reset();
+ resetParser();
return;
case CSI_PARAM:
if (ch == '~') {
events.add(csiFnKey());
- reset();
+ resetParser();
return;
}
ctrl = csiIsCtrl(params.get(1));
}
events.add(new TKeypressEvent(kbUp, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'B':
// Down
ctrl = csiIsCtrl(params.get(1));
}
events.add(new TKeypressEvent(kbDown, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'C':
// Right
ctrl = csiIsCtrl(params.get(1));
}
events.add(new TKeypressEvent(kbRight, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'D':
// Left
ctrl = csiIsCtrl(params.get(1));
}
events.add(new TKeypressEvent(kbLeft, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'H':
// Home
ctrl = csiIsCtrl(params.get(1));
}
events.add(new TKeypressEvent(kbHome, alt, ctrl, shift));
- reset();
+ resetParser();
return;
case 'F':
// End
ctrl = csiIsCtrl(params.get(1));
}
events.add(new TKeypressEvent(kbEnd, alt, ctrl, shift));
- reset();
+ resetParser();
return;
default:
break;
}
// Unknown keystroke, ignore
- reset();
+ resetParser();
return;
case MOUSE:
if (params.get(0).length() == 3) {
// We have enough to generate a mouse event
events.add(parseMouse());
- reset();
+ resetParser();
}
return;
}
/**
- * Create an xterm OSC sequence to change the window title. Note package
- * private access.
+ * Create an xterm OSC sequence to change the window title.
*
* @param title the new title
* @return the string to emit to xterm
*/
- String setTitle(final String title) {
+ private String getSetTitleString(final String title) {
return "\033]2;" + title + "\007";
}
/**
- * Create a SGR parameter sequence for a single color change. Note
- * package private access.
+ * Create a SGR parameter sequence for a single color change.
*
* @param bold if true, set bold
* @param color one of the Color.WHITE, Color.BLUE, etc. constants
* @return the string to emit to an ANSI / ECMA-style terminal,
* e.g. "\033[42m"
*/
- String color(final boolean bold, final Color color,
+ private String color(final boolean bold, final Color color,
final boolean foreground) {
return color(color, foreground, true) +
rgbColor(bold, color, foreground);
/**
* Create a SGR parameter sequence for both foreground and background
- * color change. Note package private access.
+ * color change.
*
* @param bold if true, set bold
* @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
* @return the string to emit to an ANSI / ECMA-style terminal,
* e.g. "\033[31;42m"
*/
- String color(final boolean bold, final Color foreColor,
+ private String color(final boolean bold, final Color foreColor,
final Color backColor) {
return color(foreColor, backColor, true) +
rgbColor(bold, foreColor, backColor);
/**
* Create a SGR parameter sequence for foreground, background, and
* several attributes. This sequence first resets all attributes to
- * default, then sets attributes as per the parameters. Note package
- * private access.
+ * default, then sets attributes as per the parameters.
*
* @param foreColor one of the Color.WHITE, Color.BLUE, etc. constants
* @param backColor one of the Color.WHITE, Color.BLUE, etc. constants
* @return the string to emit to an ANSI / ECMA-style terminal,
* e.g. "\033[0;1;31;42m"
*/
- String color(final Color foreColor, final Color backColor,
+ private String color(final Color foreColor, final Color backColor,
final boolean bold, final boolean reverse, final boolean blink,
final boolean underline) {
}
/**
- * Create a SGR parameter sequence to reset to defaults. Note package
- * private access.
+ * Create a SGR parameter sequence to reset to defaults.
*
* @return the string to emit to an ANSI / ECMA-style terminal,
* e.g. "\033[0m"
*/
- String normal() {
+ private String normal() {
return normal(true) + rgbColor(false, Color.WHITE, Color.BLACK);
}
}
/**
- * Create a SGR parameter sequence for enabling the visible cursor. Note
- * package private access.
+ * Create a SGR parameter sequence for enabling the visible cursor.
*
* @param on if true, turn on cursor
* @return the string to emit to an ANSI / ECMA-style terminal
*/
- String cursor(final boolean on) {
+ private String cursor(final boolean on) {
if (on && !cursorOn) {
cursorOn = true;
return "\033[?25h";
*
* @return the string to emit to an ANSI / ECMA-style terminal
*/
- public String clearAll() {
+ private String clearAll() {
return "\033[0;37;40m\033[2J";
}
/**
* Clear the line from the cursor (inclusive) to the end of the screen.
* Because some terminals use back-color-erase, set the color to
- * white-on-black beforehand. Note package private access.
+ * white-on-black beforehand.
*
* @return the string to emit to an ANSI / ECMA-style terminal
*/
- String clearRemainingLine() {
+ private String clearRemainingLine() {
return "\033[0;37;40m\033[K";
}
/**
- * Move the cursor to (x, y). Note package private access.
+ * Move the cursor to (x, y).
*
* @param x column coordinate. 0 is the left-most column.
* @param y row coordinate. 0 is the top-most row.
* @return the string to emit to an ANSI / ECMA-style terminal
*/
- String gotoXY(final int x, final int y) {
+ private String gotoXY(final int x, final int y) {
return String.format("\033[%d;%dH", y + 1, x + 1);
}