*/
package jexer.tterminal;
+import java.awt.Graphics2D;
+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;
import java.io.Writer;
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.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.*;
DCS_PARAM,
DCS_PASSTHROUGH,
DCS_IGNORE,
+ DCS_SIXEL,
SOSPMAPC_STRING,
OSC_STRING,
VT52_DIRECT_CURSOR_ADDRESS
/**
* XTERM mouse reporting protocols.
*/
- private enum MouseProtocol {
+ public enum MouseProtocol {
OFF,
X10,
NORMAL,
/**
* The type of emulator to be.
*/
- private DeviceType type = DeviceType.VT102;
+ private final DeviceType type;
/**
* The scrollback buffer characters + attributes.
*/
private MouseEncoding mouseEncoding = MouseEncoding.X10;
+ /**
+ * A terminal may request that the mouse pointer be hidden using a
+ * Privacy Message containing either "hideMousePointer" or
+ * "showMousePointer". This is currently only used within Jexer by
+ * TTerminalWindow so that only the bottom-most instance of nested
+ * Jexer's draws the mouse within its application window.
+ */
+ private boolean hideMousePointer = false;
+
/**
* 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.
*/
- private char repCh;
+ private int repCh;
/**
* VT100-style line wrapping: a character is placed in column 80 (or
* 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.
/**
* Non-csi collect buffer.
*/
- private StringBuilder collectBuffer;
+ private StringBuilder collectBuffer = new StringBuilder(128);
/**
* When true, use the G1 character set.
*/
private List<Integer> colors88;
+ /**
+ * Sixel collection buffer.
+ */
+ private StringBuilder sixelParseBuffer = new StringBuilder(2048);
+
+ /**
+ * Sixel shared palette.
+ */
+ private HashMap<Integer, java.awt.Color> sixelPalette;
+
+ /**
+ * The width of a character cell in pixels.
+ */
+ private int textWidth = 16;
+
+ /**
+ * The height of a character cell in pixels.
+ */
+ 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.
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;
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);
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();
}
if (n == 0) {
try {
- Thread.sleep(2);
+ Thread.sleep(10);
} catch (InterruptedException e) {
// SQUASH
}
} 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);
+
+ // 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++) {
+ // Special case for VT10x: 7-bit characters
+ // only.
+ if ((type == DeviceType.VT100)
+ || (type == DeviceType.VT102)
+ ) {
+ consume(readBuffer[i] & 0x7F);
+ } else {
+ consume(readBuffer[i]);
+ }
}
-
- consume((char) ch);
}
}
// Permit my enclosing UI to know that I updated.
// 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.
*
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;6c";
+ return "\033[?62;1;6;9;4;22;444c";
}
- // "I am a VT220" - 8 bit version
- return "\u009b?62;1;6c";
+ // "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);
}
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.
*
*
* @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) {
*
* @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;
- if (scrollRegionBottom < 0) {
- scrollRegionBottom = height;
+ if ((scrollRegionBottom < 0) || (scrollRegionTop > height - 1)) {
+ scrollRegionBottom = height - 1;
}
if (scrollRegionTop >= scrollRegionBottom) {
scrollRegionTop = 0;
*/
private void toGround() {
csiParams.clear();
- collectBuffer = new StringBuilder(8);
+ collectBuffer.setLength(0);
scanState = ScanState.GROUND;
}
private void resetTabStops() {
tabStops.clear();
for (int i = 0; (i * 8) <= rightMargin; i++) {
- tabStops.add(new Integer(i * 8));
+ tabStops.add(Integer.valueOf(i * 8));
}
}
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;
arrowKeyMode = ArrowKeyMode.ANSI;
keypadMode = KeypadMode.Numeric;
wrapLineFlag = false;
- if (displayListener != null) {
- width = displayListener.getDisplayWidth();
- height = displayListener.getDisplayHeight();
- rightMargin = width - 1;
- }
// Flags
shiftOut = false;
* Handle a linefeed.
*/
private void linefeed() {
-
if (currentState.cursorY < scrollRegionBottom) {
// Increment screen y
currentState.cursorY++;
*
* @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()) {
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--;
+ }
}
}
}
*
* @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",
*
* @param keypress keypress received from the local user
*/
- public void keypress(final TKeypress keypress) {
+ private void keypress(final TKeypress keypress) {
writeRemote(keypressToString(keypress));
}
* @param keypress keypress received from the local user
* @return string to transmit to the remote side
*/
+ @SuppressWarnings("fallthrough")
private String keypressToString(final TKeypress keypress) {
if ((fullDuplex == false) && (!keypress.isFnKey())) {
* 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))) {
// 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();
}
// 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 "";
* @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) {
* @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;
switch (currentState.glLockshift) {
case G1_GR:
- assert (false);
+ throw new IllegalArgumentException("programming bug");
case G2_GR:
- assert (false);
+ throw new IllegalArgumentException("programming bug");
case G3_GR:
- assert (false);
+ throw new IllegalArgumentException("programming bug");
case G2_GL:
// LS2
switch (currentState.grLockshift) {
case G2_GL:
- assert (false);
+ throw new IllegalArgumentException("programming bug");
case G3_GL:
- assert (false);
+ throw new IllegalArgumentException("programming bug");
case G1_GR:
// LS1R
*/
private void param(final byte ch) {
if (csiParams.size() == 0) {
- csiParams.add(new Integer(0));
+ csiParams.add(Integer.valueOf(0));
}
Integer x = csiParams.get(csiParams.size() - 1);
if ((ch >= '0') && (ch <= '9')) {
csiParams.set(csiParams.size() - 1, x);
}
- if (ch == ';') {
- csiParams.add(new Integer(0));
+ if ((ch == ';') && (csiParams.size() < 16)) {
+ csiParams.add(Integer.valueOf(0));
}
}
*/
private void setToggle(final boolean value) {
boolean decPrivateModeFlag = false;
+
for (int i = 0; i < collectBuffer.length(); i++) {
if (collectBuffer.charAt(i) == '?') {
decPrivateModeFlag = true;
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)
}
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;
* 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);
if (collectBuffer.charAt(0) == '>') {
extendedFlag = 1;
if (collectBuffer.length() >= 2) {
- i = Integer.parseInt(args.toString());
+ i = Integer.parseInt(args);
}
} else if (collectBuffer.charAt(0) == '=') {
extendedFlag = 2;
if (collectBuffer.length() >= 2) {
- i = Integer.parseInt(args.toString());
+ i = Integer.parseInt(args);
}
} else {
// Unknown code, bail out
// 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;
args = collectBuffer.substring(0, collectBuffer.length() - 2);
}
- String [] p = args.toString().split(";");
+ String [] p = args.split(";");
if (p.length > 0) {
if ((p[0].equals("0")) || (p[0].equals("2"))) {
if (p.length > 1) {
}
}
}
+
+ 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
}
}
+ /**
+ * Handle the SCAN_SOSPMAPC_STRING state. This is currently only used by
+ * Jexer ECMA48Terminal to talk to ECMA48.
+ *
+ * @param pmChar the character received from the remote side
+ */
+ private void pmPut(final char pmChar) {
+ // System.err.println("pmPut: " + pmChar);
+
+ // Collect first
+ collectBuffer.append(pmChar);
+
+ // Xterm cases...
+ if (collectBuffer.toString().endsWith("\033\\")) {
+ String arg = null;
+ arg = collectBuffer.substring(0, collectBuffer.length() - 2);
+
+ // System.err.println("arg: '" + arg + "'");
+
+ if (arg.equals("hideMousePointer")) {
+ hideMousePointer = true;
+ }
+ if (arg.equals("showMousePointer")) {
+ hideMousePointer = false;
+ }
+
+ // Go to SCAN_GROUND state
+ toGround();
+ return;
+ }
+ }
+
+ /**
+ * Perform xterm window operations.
+ */
+ private void xtermWindowOps() {
+ boolean xtermPrivateModeFlag = false;
+
+ for (int i = 0; i < collectBuffer.length(); i++) {
+ if (collectBuffer.charAt(i) == '?') {
+ xtermPrivateModeFlag = true;
+ break;
+ }
+ }
+
+ int i = getCsiParam(0, 0);
+
+ if (!xtermPrivateModeFlag) {
+ 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(final int ch) {
// DEBUG
- // System.err.printf("%c", ch);
-
- // Special case for VT10x: 7-bit characters only
- if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) {
- ch = (char)(ch & 0x7F);
- }
+ // System.err.printf("%c STATE = %s\n", ch, scanState);
// Special "anywhere" states
// 0x1B == ESCAPE
if (ch == 0x1B) {
if ((type == DeviceType.XTERM)
- && (scanState == ScanState.OSC_STRING)
+ && ((scanState == ScanState.OSC_STRING)
+ || (scanState == ScanState.DCS_SIXEL)
+ || (scanState == ScanState.SOSPMAPC_STRING))
) {
// Xterm can pass ESCAPE to its OSC sequence.
+ // Xterm can pass ESCAPE to its DCS sequence.
+ // Jexer can pass ESCAPE to its PM sequence.
} else if ((scanState != ScanState.DCS_ENTRY)
&& (scanState != ScanState.DCS_INTERMEDIATE)
&& (scanState != ScanState.DCS_IGNORE)
&& (scanState != ScanState.DCS_PARAM)
&& (scanState != ScanState.DCS_PASSTHROUGH)
) {
-
scanState = ScanState.ESCAPE;
return;
}
// 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
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;
}
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
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;
}
// 3C-3F --> collect, then switch to CSI_PARAM
if ((ch >= 0x3C) && (ch <= 0x3F)) {
- collect(ch);
+ collect((char) ch);
scanState = ScanState.CSI_PARAM;
}
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':
}
break;
case 't':
+ if (type == DeviceType.XTERM) {
+ // Window operations
+ xtermWindowOps();
+ }
break;
case 'u':
// Restore cursor (ANSI.SYS)
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;
}
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':
decstbm();
break;
case 's':
+ break;
case 't':
+ if (type == DeviceType.XTERM) {
+ // Window operations
+ xtermWindowOps();
+ }
+ break;
case 'u':
case 'v':
case 'w':
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
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
// 0x1B 0x5C goes to GROUND
if (ch == 0x1B) {
- collect(ch);
+ collect((char) ch);
}
if (ch == 0x5C) {
if ((collectBuffer.length() > 0)
// 20-2F --> collect, then switch to DCS_INTERMEDIATE
if ((ch >= 0x20) && (ch <= 0x2F)) {
- collect(ch);
+ collect((char) ch);
scanState = ScanState.DCS_INTERMEDIATE;
}
// 3C-3F --> collect, then switch to DCS_PARAM
if ((ch >= 0x3C) && (ch <= 0x3F)) {
- collect(ch);
+ collect((char) ch);
scanState = ScanState.DCS_PARAM;
}
scanState = ScanState.DCS_IGNORE;
}
- // 0x40-7E goes to DCS_PASSTHROUGH
- if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x71 goes to DCS_SIXEL
+ if (ch == 0x71) {
+ sixelParseBuffer.setLength(0);
+ scanState = ScanState.DCS_SIXEL;
+ } else if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x40-7E goes to DCS_PASSTHROUGH
scanState = ScanState.DCS_PASSTHROUGH;
}
return;
// 0x1B 0x5C goes to GROUND
if (ch == 0x1B) {
- collect(ch);
+ collect((char) ch);
}
if (ch == 0x5C) {
if ((collectBuffer.length() > 0)
// 0x1B 0x5C goes to GROUND
if (ch == 0x1B) {
- collect(ch);
+ collect((char) ch);
}
if (ch == 0x5C) {
if ((collectBuffer.length() > 0)
// 20-2F --> collect, then switch to DCS_INTERMEDIATE
if ((ch >= 0x20) && (ch <= 0x2F)) {
- collect(ch);
+ collect((char) ch);
scanState = ScanState.DCS_INTERMEDIATE;
}
scanState = ScanState.DCS_IGNORE;
}
- // 0x40-7E goes to DCS_PASSTHROUGH
- if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x71 goes to DCS_SIXEL
+ if (ch == 0x71) {
+ sixelParseBuffer.setLength(0);
+ scanState = ScanState.DCS_SIXEL;
+ } else if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x40-7E goes to DCS_PASSTHROUGH
scanState = ScanState.DCS_PASSTHROUGH;
}
return;
// 0x1B 0x5C goes to GROUND
if (ch == 0x1B) {
- collect(ch);
+ collect((char) ch);
}
if (ch == 0x5C) {
if ((collectBuffer.length() > 0)
}
// 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;
}
return;
+ case DCS_SIXEL:
+ // 0x9C goes to GROUND
+ 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)
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B)
+ ) {
+ parseSixel();
+ toGround();
+ return;
+ }
+ }
+
+ // 00-17, 19, 1C-1F, 20-7E --> put
+ if ((ch <= 0x17)
+ || (ch == 0x19)
+ || ((ch >= 0x1C) && (ch <= 0x1F))
+ || ((ch >= 0x20) && (ch <= 0x7E))
+ ) {
+ sixelParseBuffer.append((char) ch);
+ }
+
+ // 7F --> ignore
+ return;
+
case SOSPMAPC_STRING:
// 00-17, 19, 1C-1F, 20-7F --> ignore
+ // Special case for Jexer: PM can pass one control character
+ if (ch == 0x1B) {
+ pmPut((char) ch);
+ }
+
+ if ((ch >= 0x20) && (ch <= 0x7F)) {
+ pmPut((char) ch);
+ }
+
// 0x9C goes to GROUND
if (ch == 0x9C) {
toGround();
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
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.
return currentState.cursorY;
}
+ /**
+ * Returns true if this terminal has requested the mouse pointer be
+ * hidden.
+ *
+ * @return true if this terminal has requested the mouse pointer be
+ * hidden
+ */
+ public final boolean hasHiddenMousePointer() {
+ return hideMousePointer;
+ }
+
+ /**
+ * Get the mouse protocol.
+ *
+ * @return MouseProtocol.OFF, MouseProtocol.X10, etc.
+ */
+ public MouseProtocol getMouseProtocol() {
+ return mouseProtocol;
+ }
+
+ /**
+ * 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.
+ *
+ * @param textWidth the width in pixels of a character cell
+ */
+ public void setTextWidth(final int textWidth) {
+ this.textWidth = textWidth;
+ }
+
+ /**
+ * Set the height of a character cell in pixels.
+ *
+ * @param textHeight the height in pixels of a character cell
+ */
+ public void setTextHeight(final int textHeight) {
+ this.textHeight = textHeight;
+ }
+
+ /**
+ * Parse a sixel string into a bitmap image, and overlay that image onto
+ * the text cells.
+ */
+ private void parseSixel() {
+
+ /*
+ System.err.println("parseSixel(): '" + sixelParseBuffer.toString()
+ + "'");
+ */
+
+ Sixel sixel = new Sixel(sixelParseBuffer.toString(), sixelPalette);
+ BufferedImage image = sixel.getImage();
+
+ // System.err.println("parseSixel(): image " + image);
+
+ if (image == null) {
+ // 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;
+ }
+
+ 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);
+ }
+ }
+
+ 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 {
+ java.util.Base64.Decoder base64 = java.util.Base64.getDecoder();
+ byte [] bytes = base64.decode(data);
+
+ 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:
+ *
+ * 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;
+ int y0 = currentState.cursorY;
+ for (int y = 0; y < cellRows; y++) {
+ for (int x = 0; x < cellColumns; x++) {
+ 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;
+ }
+ // Room for more image on the visible screen.
+ currentState.cursorX++;
+ }
+ 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);
+ }
+
+ if (scroll == false) {
+ cursorPosition(y0, x0);
+ }
+
+ }
+
}