+ /**
+ * The current editing Y position.
+ */
+ public int cursorY = 0;
+
+ /**
+ * Which character set is currently selected in G0.
+ */
+ public CharacterSet g0Charset = CharacterSet.US;
+
+ /**
+ * Which character set is currently selected in G1.
+ */
+ public CharacterSet g1Charset = CharacterSet.DRAWING;
+
+ /**
+ * Which character set is currently selected in G2.
+ */
+ public CharacterSet g2Charset = CharacterSet.US;
+
+ /**
+ * Which character set is currently selected in G3.
+ */
+ public CharacterSet g3Charset = CharacterSet.US;
+
+ /**
+ * Which character set is currently selected in GR.
+ */
+ public CharacterSet grCharset = CharacterSet.DRAWING;
+
+ /**
+ * The current drawing attributes.
+ */
+ public CellAttributes attr;
+
+ /**
+ * GL lockshift mode.
+ */
+ public LockshiftMode glLockshift = LockshiftMode.NONE;
+
+ /**
+ * GR lockshift mode.
+ */
+ public LockshiftMode grLockshift = LockshiftMode.NONE;
+
+ /**
+ * Line wrap.
+ */
+ public boolean lineWrap = true;
+
+ /**
+ * Reset to defaults.
+ */
+ public void reset() {
+ originMode = false;
+ cursorX = 0;
+ cursorY = 0;
+ g0Charset = CharacterSet.US;
+ g1Charset = CharacterSet.DRAWING;
+ g2Charset = CharacterSet.US;
+ g3Charset = CharacterSet.US;
+ grCharset = CharacterSet.DRAWING;
+ attr = new CellAttributes();
+ glLockshift = LockshiftMode.NONE;
+ grLockshift = LockshiftMode.NONE;
+ lineWrap = true;
+ }
+
+ /**
+ * Copy attributes from another instance.
+ *
+ * @param that the other instance to match
+ */
+ public void setTo(final SaveableState that) {
+ this.originMode = that.originMode;
+ this.cursorX = that.cursorX;
+ this.cursorY = that.cursorY;
+ this.g0Charset = that.g0Charset;
+ this.g1Charset = that.g1Charset;
+ this.g2Charset = that.g2Charset;
+ this.g3Charset = that.g3Charset;
+ this.grCharset = that.grCharset;
+ this.attr = new CellAttributes();
+ this.attr.setTo(that.attr);
+ this.glLockshift = that.glLockshift;
+ this.grLockshift = that.grLockshift;
+ this.lineWrap = that.lineWrap;
+ }
+
+ /**
+ * Public constructor.
+ */
+ public SaveableState() {
+ reset();
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor.
+ *
+ * @param type one of the DeviceType constants to select VT100, VT102,
+ * VT220, or XTERM
+ * @param inputStream an InputStream connected to the remote side. For
+ * type == XTERM, inputStream is converted to a Reader with UTF-8
+ * encoding.
+ * @param outputStream an OutputStream connected to the remote user. For
+ * type == XTERM, outputStream is converted to a Writer with UTF-8
+ * encoding.
+ * @param displayListener a callback to the outer display, or null for
+ * default VT100 behavior
+ * @throws UnsupportedEncodingException if an exception is thrown when
+ * creating the InputStreamReader
+ */
+ public ECMA48(final DeviceType type, final InputStream inputStream,
+ final OutputStream outputStream, final DisplayListener displayListener)
+ throws UnsupportedEncodingException {
+
+ assert (inputStream != null);
+ assert (outputStream != null);
+
+ csiParams = new ArrayList<Integer>();
+ tabStops = new ArrayList<Integer>();
+ scrollback = new ArrayList<DisplayLine>();
+ display = new ArrayList<DisplayLine>();
+
+ this.type = type;
+ if (inputStream instanceof TimeoutInputStream) {
+ this.inputStream = (TimeoutInputStream)inputStream;
+ } else {
+ this.inputStream = new TimeoutInputStream(inputStream, 2000);
+ }
+ if (type == DeviceType.XTERM) {
+ this.input = new InputStreamReader(this.inputStream, "UTF-8");
+ this.output = new OutputStreamWriter(new
+ BufferedOutputStream(outputStream), "UTF-8");
+ this.outputStream = null;
+ } else {
+ this.output = null;
+ this.outputStream = new BufferedOutputStream(outputStream);
+ }
+ this.displayListener = displayListener;
+
+ reset();
+ for (int i = 0; i < height; i++) {
+ display.add(new DisplayLine(currentState.attr));
+ }
+
+ // Spin up the input reader
+ readerThread = new Thread(this);
+ readerThread.start();
+ }
+
+ // ------------------------------------------------------------------------
+ // Runnable ---------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Read function runs on a separate thread.
+ */
+ public final void run() {
+ boolean utf8 = false;
+ boolean done = false;
+
+ if (type == DeviceType.XTERM) {
+ utf8 = true;
+ }
+
+ // available() will often return > 1, so we need to read in chunks to
+ // stay caught up.
+ char [] readBufferUTF8 = null;
+ byte [] readBuffer = null;
+ if (utf8) {
+ readBufferUTF8 = new char[2048];
+ } else {
+ readBuffer = new byte[2048];
+ }
+
+ while (!done && !stopReaderThread) {
+ synchronized (userQueue) {
+ while (userQueue.size() > 0) {
+ handleUserEvent(userQueue.remove(0));
+ }
+ }
+
+ try {
+ int n = inputStream.available();
+
+ // System.err.printf("available() %d\n", n); System.err.flush();
+ if (utf8) {
+ if (readBufferUTF8.length < n) {
+ // The buffer wasn't big enough, make it huger
+ int newSizeHalf = Math.max(readBufferUTF8.length,
+ n);
+
+ readBufferUTF8 = new char[newSizeHalf * 2];
+ }
+ } else {
+ if (readBuffer.length < n) {
+ // The buffer wasn't big enough, make it huger
+ int newSizeHalf = Math.max(readBuffer.length, n);
+ readBuffer = new byte[newSizeHalf * 2];
+ }
+ }
+ if (n == 0) {
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ continue;
+ }
+
+ int rc = -1;
+ try {
+ if (utf8) {
+ rc = input.read(readBufferUTF8, 0,
+ readBufferUTF8.length);
+ } else {
+ rc = inputStream.read(readBuffer, 0,
+ readBuffer.length);
+ }
+ } catch (ReadTimeoutException e) {
+ rc = 0;
+ }
+
+ // System.err.printf("read() %d\n", rc); System.err.flush();
+ if (rc == -1) {
+ // This is EOF
+ done = true;
+ } else {
+ // Don't step on UI events
+ synchronized (this) {
+ if (utf8) {
+ for (int i = 0; i < rc;) {
+ int ch = Character.codePointAt(readBufferUTF8,
+ i);
+ i += Character.charCount(ch);
+ consume(ch);
+ }
+ } else {
+ for (int i = 0; i < rc; i++) {
+ consume(readBuffer[i]);
+ }
+ }
+ }
+ // Permit my enclosing UI to know that I updated.
+ if (displayListener != null) {
+ displayListener.displayChanged();
+ }
+ }
+ // System.err.println("end while loop"); System.err.flush();
+ } catch (IOException e) {
+ 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))
+
+ // Let the rest of the world know that I am done.
+ stopReaderThread = true;
+
+ try {
+ inputStream.cancelRead();
+ inputStream.close();
+ inputStream = null;
+ } catch (IOException e) {
+ // SQUASH
+ }
+ try {
+ input.close();
+ input = null;
+ } catch (IOException e) {
+ // SQUASH
+ }
+
+ // Permit my enclosing UI to know that I updated.
+ if (displayListener != null) {
+ displayListener.displayChanged();
+ }
+
+ // System.err.println("*** run() exiting..."); System.err.flush();
+ }
+
+ // ------------------------------------------------------------------------
+ // 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.
+ *
+ * @return string to send to remote side that is appropriate for the
+ * this.type
+ */
+ private String deviceTypeResponse() {
+ switch (type) {
+ case VT100:
+ // "I am a VT100 with advanced video option" (often VT102)
+ return "\033[?1;2c";
+
+ case VT102:
+ // "I am a VT102"
+ return "\033[?6c";
+
+ case VT220:
+ case XTERM:
+ // "I am a VT220" - 7 bit version
+ if (!s8c1t) {
+ return "\033[?62;1;6;9;4;22c";
+ // return "\033[?62;1;6;9;4;22;444c";
+ }
+ // "I am a VT220" - 8 bit version
+ return "\u009b?62;1;6;9;4;22c";
+ // return "\u009b?62;1;6;9;4;22;444c";
+ default:
+ throw new IllegalArgumentException("Invalid device type: " + type);
+ }
+ }
+
+ /**
+ * Return the proper TERM environment variable for this device type.
+ *
+ * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc.
+ * @return "vt100", "xterm", etc.
+ */
+ public static String deviceTypeTerm(final DeviceType deviceType) {
+ switch (deviceType) {
+ case VT100:
+ return "vt100";
+
+ case VT102:
+ return "vt102";
+
+ case VT220:
+ return "vt220";
+
+ case XTERM:
+ return "xterm";
+
+ default:
+ throw new IllegalArgumentException("Invalid device type: "
+ + deviceType);
+ }
+ }
+
+ /**
+ * Return the proper LANG for this device type. Only XTERM devices know
+ * about UTF-8, the others are defined by their standard to be either
+ * 7-bit or 8-bit characters only.
+ *
+ * @param deviceType DeviceType.VT100, DeviceType, XTERM, etc.
+ * @param baseLang a base language without UTF-8 flag such as "C" or
+ * "en_US"
+ * @return "en_US", "en_US.UTF-8", etc.
+ */
+ public static String deviceTypeLang(final DeviceType deviceType,
+ final String baseLang) {
+
+ switch (deviceType) {
+
+ case VT100:
+ case VT102:
+ case VT220:
+ return baseLang;
+
+ case XTERM:
+ return baseLang + ".UTF-8";
+
+ default:
+ throw new IllegalArgumentException("Invalid device type: "
+ + deviceType);
+ }
+ }
+
+ /**
+ * Write a string directly to the remote side.
+ *
+ * @param str string to send
+ */
+ public void writeRemote(final String str) {
+ if (stopReaderThread) {
+ // Reader hit EOF, bail out now.
+ close();
+ return;
+ }
+
+ // System.err.printf("writeRemote() '%s'\n", str);
+
+ switch (type) {
+ case VT100:
+ case VT102:
+ case VT220:
+ if (outputStream == null) {
+ return;
+ }
+ try {
+ outputStream.flush();
+ for (int i = 0; i < str.length(); i++) {
+ outputStream.write(str.charAt(i));
+ }
+ outputStream.flush();
+ } catch (IOException e) {
+ // Assume EOF
+ close();
+ }
+ break;
+ case XTERM:
+ if (output == null) {
+ return;
+ }
+ try {
+ output.flush();
+ output.write(str);
+ output.flush();
+ } catch (IOException e) {
+ // Assume EOF
+ close();
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid device type: " + type);
+ }
+ }
+
+ /**
+ * Close the input and output streams and stop the reader thread. Note
+ * that it is safe to call this multiple times.
+ */
+ public final void close() {
+
+ // Tell the reader thread to stop looking at input. It will close
+ // the input streams.
+ if (stopReaderThread == false) {
+ stopReaderThread = true;
+ try {
+ readerThread.join(1000);
+ } catch (InterruptedException e) {
+ // SQUASH
+ }
+ }
+
+ // Now close the output stream.
+ switch (type) {
+ case VT100:
+ case VT102:
+ case VT220:
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ // SQUASH
+ }
+ outputStream = null;
+ }
+ break;
+ case XTERM:
+ if (outputStream != null) {
+ try {
+ outputStream.close();
+ } catch (IOException e) {
+ // SQUASH
+ }
+ outputStream = null;
+ }
+ if (output != null) {
+ try {
+ output.close();
+ } catch (IOException e) {
+ // SQUASH
+ }
+ output = null;
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid device type: " +
+ type);
+ }
+ }
+
+ /**
+ * See if the reader thread is still running.
+ *
+ * @return if true, we are still connected to / reading from the remote
+ * side
+ */
+ public final boolean isReading() {
+ return (!stopReaderThread);
+ }
+
+ /**
+ * Obtain a new blank display line for an external user
+ * (e.g. TTerminalWindow).
+ *
+ * @return new blank line
+ */
+ public final DisplayLine getBlankDisplayLine() {
+ return new DisplayLine(currentState.attr);
+ }
+
+ /**
+ * Get the scrollback buffer.
+ *
+ * @return the scrollback buffer
+ */
+ public final List<DisplayLine> getScrollbackBuffer() {
+ return scrollback;
+ }
+
+ /**
+ * Get the display buffer.
+ *
+ * @return the display buffer
+ */
+ public final List<DisplayLine> getDisplayBuffer() {
+ 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) {