*
* The MIT License (MIT)
*
- * Copyright (C) 2017 Kevin Lamonte
+ * Copyright (C) 2019 Kevin Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
*/
package jexer.tterminal;
+import java.awt.Graphics2D;
+import java.awt.image.BufferedImage;
import java.io.BufferedOutputStream;
+import java.io.CharArrayWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
-import java.util.LinkedList;
+import java.util.HashMap;
import java.util.List;
import jexer.TKeypress;
DCS_PARAM,
DCS_PASSTHROUGH,
DCS_IGNORE,
+ DCS_SIXEL,
SOSPMAPC_STRING,
OSC_STRING,
VT52_DIRECT_CURSOR_ADDRESS
/**
* The scrollback buffer characters + attributes.
*/
- private volatile List<DisplayLine> scrollback;
+ private volatile ArrayList<DisplayLine> scrollback;
/**
* The raw display buffer characters + attributes.
*/
- private volatile List<DisplayLine> display;
+ private volatile ArrayList<DisplayLine> display;
+
+ /**
+ * The maximum number of lines in the scrollback buffer.
+ */
+ private int maxScrollback = 10000;
/**
* The terminal's input. For type == XTERM, this is an InputStreamReader
*/
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 List<Integer> colors88;
+ /**
+ * Sixel collection buffer.
+ */
+ private StringBuilder sixelParseBuffer;
+
+ /**
+ * The width of a character cell in pixels.
+ */
+ private int textWidth = 16;
+
+ /**
+ * The height of a character cell in pixels.
+ */
+ private int textHeight = 20;
+
/**
* DECSC/DECRC save/restore a subset of the total state. This class
* encapsulates those specific flags/modes.
csiParams = new ArrayList<Integer>();
tabStops = new ArrayList<Integer>();
- scrollback = new LinkedList<DisplayLine>();
- display = new LinkedList<DisplayLine>();
+ scrollback = new ArrayList<DisplayLine>();
+ display = new ArrayList<DisplayLine>();
this.type = type;
if (inputStream instanceof TimeoutInputStream) {
ch = readBuffer[i];
}
- consume((char)ch);
+ consume((char) ch);
}
}
// Permit my enclosing UI to know that I updated.
}
// System.err.println("end while loop"); System.err.flush();
} catch (IOException e) {
- e.printStackTrace();
done = true;
+
+ // This is an unusual case. We want to see the stack trace,
+ // but it is related to the spawned process rather than the
+ // actual UI. We will generate the stack trace, and consume
+ // it as though it was emitted by the shell.
+ CharArrayWriter writer= new CharArrayWriter();
+ // Send a ST and RIS to clear the emulator state.
+ try {
+ writer.write("\033\\\033c");
+ writer.write("\n-----------------------------------\n");
+ e.printStackTrace(new PrintWriter(writer));
+ writer.write("\n-----------------------------------\n");
+ } catch (IOException e2) {
+ // SQUASH
+ }
+ char [] stackTrace = writer.toCharArray();
+ for (int i = 0; i < stackTrace.length; i++) {
+ if (stackTrace[i] == '\n') {
+ consume('\r');
+ }
+ consume(stackTrace[i]);
+ }
}
} // while ((done == false) && (stopReaderThread == false))
try {
readerThread.join(1000);
} catch (InterruptedException e) {
- e.printStackTrace();
+ // SQUASH
}
}
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));
}
}
private void newDisplayLine() {
// Scroll the top line off into the scrollback buffer
scrollback.add(display.get(0));
+ if (scrollback.size() > maxScrollback) {
+ scrollback.remove(0);
+ scrollback.trimToSize();
+ }
display.remove(0);
+ display.trimToSize();
DisplayLine line = new DisplayLine(currentState.attr);
line.setReverseColor(reverseVideo);
display.add(line);
* @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())) {
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
display.size());
List<DisplayLine> displayMiddle = display.subList(regionBottom + 1
- remaining, regionBottom + 1);
- display = new LinkedList<DisplayLine>(displayTop);
+ display = new ArrayList<DisplayLine>(displayTop);
display.addAll(displayMiddle);
for (int i = 0; i < n; i++) {
DisplayLine line = new DisplayLine(currentState.attr);
display.size());
List<DisplayLine> displayMiddle = display.subList(regionTop,
regionTop + remaining);
- display = new LinkedList<DisplayLine>(displayTop);
+ display = new ArrayList<DisplayLine>(displayTop);
for (int i = 0; i < n; i++) {
DisplayLine line = new DisplayLine(currentState.attr);
line.setReverseColor(reverseVideo);
*/
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));
}
}
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
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) {
}
}
+ /**
+ * 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;
+ }
+ }
+
/**
* Run this input character through the ECMA48 state machine.
*
private void consume(char ch) {
// DEBUG
- // System.err.printf("%c", ch);
+ // System.err.printf("%c STATE = %s\n", ch, scanState);
// Special case for VT10x: 7-bit characters only
if ((type == DeviceType.VT100) || (type == DeviceType.VT102)) {
// 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;
}
scanState = ScanState.DCS_IGNORE;
}
- // 0x40-7E goes to DCS_PASSTHROUGH
- if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x71 goes to DCS_SIXEL
+ if (ch == 0x71) {
+ sixelParseBuffer = new StringBuilder();
+ scanState = ScanState.DCS_SIXEL;
+ } else if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x40-7E goes to DCS_PASSTHROUGH
scanState = ScanState.DCS_PASSTHROUGH;
}
return;
scanState = ScanState.DCS_IGNORE;
}
- // 0x40-7E goes to DCS_PASSTHROUGH
- if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x71 goes to DCS_SIXEL
+ if (ch == 0x71) {
+ sixelParseBuffer = new StringBuilder();
+ scanState = ScanState.DCS_SIXEL;
+ } else if ((ch >= 0x40) && (ch <= 0x7E)) {
+ // 0x40-7E goes to DCS_PASSTHROUGH
scanState = ScanState.DCS_PASSTHROUGH;
}
return;
return;
+ case DCS_SIXEL:
+ // 0x9C goes to GROUND
+ if (ch == 0x9C) {
+ parseSixel();
+ toGround();
+ }
+
+ // 0x1B 0x5C goes to GROUND
+ if (ch == 0x1B) {
+ collect(ch);
+ }
+ if (ch == 0x5C) {
+ if ((collectBuffer.length() > 0)
+ && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B)
+ ) {
+ parseSixel();
+ toGround();
+ }
+ }
+
+ // 00-17, 19, 1C-1F, 20-7E --> put
+ if (ch <= 0x17) {
+ sixelParseBuffer.append(ch);
+ return;
+ }
+ if (ch == 0x19) {
+ sixelParseBuffer.append(ch);
+ return;
+ }
+ if ((ch >= 0x1C) && (ch <= 0x1F)) {
+ sixelParseBuffer.append(ch);
+ return;
+ }
+ if ((ch >= 0x20) && (ch <= 0x7E)) {
+ sixelParseBuffer.append(ch);
+ return;
+ }
+
+ // 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(ch);
+ }
+
+ if ((ch >= 0x20) && (ch <= 0x7F)) {
+ pmPut(ch);
+ }
+
// 0x9C goes to GROUND
if (ch == 0x9C) {
toGround();
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;
+ }
+
+ // ------------------------------------------------------------------------
+ // Sixel support ----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * 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());
+ BufferedImage image = sixel.getImage();
+
+ System.err.println("parseSixel(): image " + image);
+
+ if (image == null) {
+ // Sixel data was malformed in some way, bail out.
+ return;
+ }
+
+ /*
+ * 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;
+ 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]);
+ }
+ cursorRight(1, false);
+ }
+ linefeed();
+ cursorPosition(currentState.cursorY, x0);
+ }
+
+ }
+
}