/* * Jexer - Java Text User Interface * * The MIT License (MIT) * * 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"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. * * @author Kevin Lamonte [kevin.lamonte@gmail.com] * @version 1 */ package jexer.tterminal; import java.awt.Graphics; 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.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.HashMap; import java.util.List; import javax.imageio.ImageIO; import jexer.TKeypress; 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.*; /** * This implements a complex ECMA-48/ISO 6429/ANSI X3.64 type console, * including a scrollback buffer. * *

* It currently implements VT100, VT102, VT220, and XTERM with the following * caveats: * *

* - The vttest scenario for VT220 8-bit controls (11.1.2.3) reports a * failure with XTERM. This is due to vttest failing to decode the UTF-8 * stream. * *

* - Smooth scrolling, printing, keyboard locking, keyboard leds, and tests * from VT100 are not supported. * *

* - User-defined keys (DECUDK), downloadable fonts (DECDLD), and VT100/ANSI * compatibility mode (DECSCL) from VT220 are not supported. (Also, * because DECSCL is not supported, it will fail the last part of the * vttest "Test of VT52 mode" if DeviceType is set to VT220.) * *

* - Numeric/application keys from the number pad are not supported because * they are not exposed from the TKeypress API. * *

* - VT52 HOLD SCREEN mode is not supported. * *

* - In VT52 graphics mode, the 3/, 5/, and 7/ characters (fraction * numerators) are not rendered correctly. * *

* - All data meant for the 'printer' (CSI Pc ? i) is discarded. */ public class ECMA48 implements Runnable { // ------------------------------------------------------------------------ // Constants -------------------------------------------------------------- // ------------------------------------------------------------------------ /** * The emulator can emulate several kinds of terminals. */ public enum DeviceType { /** * DEC VT100 but also including the three VT102 functions. */ VT100, /** * DEC VT102. */ VT102, /** * DEC VT220. */ VT220, /** * A subset of xterm. */ XTERM } /** * Parser character scan states. */ private enum ScanState { GROUND, ESCAPE, ESCAPE_INTERMEDIATE, CSI_ENTRY, CSI_PARAM, CSI_INTERMEDIATE, CSI_IGNORE, DCS_ENTRY, DCS_INTERMEDIATE, DCS_PARAM, DCS_PASSTHROUGH, DCS_IGNORE, DCS_SIXEL, SOSPMAPC_STRING, OSC_STRING, VT52_DIRECT_CURSOR_ADDRESS } /** * The selected number pad mode (DECKPAM, DECKPNM). We record this, but * can't really use it in keypress() because we do not see number pad * events from TKeypress. */ private enum KeypadMode { Application, Numeric } /** * Arrow keys can emit three different sequences (DECCKM or VT52 * submode). */ private enum ArrowKeyMode { VT52, ANSI, VT100 } /** * Available character sets for GL, GR, G0, G1, G2, G3. */ private enum CharacterSet { US, UK, DRAWING, ROM, ROM_SPECIAL, VT52_GRAPHICS, DEC_SUPPLEMENTAL, NRC_DUTCH, NRC_FINNISH, NRC_FRENCH, NRC_FRENCH_CA, NRC_GERMAN, NRC_ITALIAN, NRC_NORWEGIAN, NRC_SPANISH, NRC_SWEDISH, NRC_SWISS } /** * Single-shift states used by the C1 control characters SS2 (0x8E) and * SS3 (0x8F). */ private enum Singleshift { NONE, SS2, SS3 } /** * VT220+ lockshift states. */ private enum LockshiftMode { NONE, G1_GR, G2_GR, G2_GL, G3_GR, G3_GL } /** * XTERM mouse reporting protocols. */ public enum MouseProtocol { OFF, X10, NORMAL, BUTTONEVENT, ANYEVENT } /** * XTERM mouse reporting encodings. */ private enum MouseEncoding { X10, UTF8, SGR } // ------------------------------------------------------------------------ // Variables -------------------------------------------------------------- // ------------------------------------------------------------------------ /** * The enclosing listening object. */ private DisplayListener displayListener; /** * When true, the reader thread is expected to exit. */ private volatile boolean stopReaderThread = false; /** * The reader thread. */ private Thread readerThread = null; /** * The type of emulator to be. */ private final DeviceType type; /** * The scrollback buffer characters + attributes. */ private volatile ArrayList scrollback; /** * The raw display buffer characters + attributes. */ private volatile ArrayList display; /** * The maximum number of lines in the scrollback buffer. */ private int scrollbackMax = 10000; /** * The terminal's input. For type == XTERM, this is an InputStreamReader * with UTF-8 encoding. */ private Reader input; /** * The terminal's raw InputStream. This is used for type != XTERM. */ private volatile TimeoutInputStream inputStream; /** * The terminal's output. For type == XTERM, this wraps an * OutputStreamWriter with UTF-8 encoding. */ private Writer output; /** * The terminal's raw OutputStream. This is used for type != XTERM. */ private OutputStream outputStream; /** * Current scanning state. */ private ScanState scanState; /** * Which mouse protocol is active. */ private MouseProtocol mouseProtocol = MouseProtocol.OFF; /** * Which mouse encoding is active. */ 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 = 80; /** * Physical display height. We start at 80x24, but the user can resize * us bigger/smaller. */ private int height = 24; /** * Top margin of the scrolling region. */ private int scrollRegionTop = 0; /** * Bottom margin of the scrolling region. */ 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 = 79; /** * Last character printed. */ 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 = false; /** * VT220 single shift flag. */ private Singleshift singleshift = Singleshift.NONE; /** * true = insert characters, false = overwrite. */ private boolean insertMode = false; /** * VT52 mode as selected by DECANM. True means VT52, false means * ANSI. Default is ANSI. */ private boolean vt52Mode = false; /** * Visible cursor (DECTCEM). */ private boolean cursorVisible = true; /** * Screen title as set by the xterm OSC sequence. Lots of applications * send a screenTitle regardless of whether it is an xterm client or not. */ private String screenTitle = ""; /** * Parameter characters being collected. */ private List csiParams; /** * Non-csi collect buffer. */ private StringBuilder collectBuffer = new StringBuilder(128); /** * When true, use the G1 character set. */ private boolean shiftOut = false; /** * Horizontal tab stop locations. */ private List tabStops; /** * S8C1T. True means 8bit controls, false means 7bit controls. */ private boolean s8c1t = false; /** * Printer mode. True means send all output to printer, which discards * it. */ private boolean printerControllerMode = false; /** * LMN line mode. If true, linefeed() puts the cursor on the first * column of the next line. If false, linefeed() puts the cursor one * line down on the current line. The default is false. */ private boolean newLineMode = false; /** * Whether arrow keys send ANSI, VT100, or VT52 sequences. */ private ArrowKeyMode arrowKeyMode; /** * Whether number pad keys send VT100 or VT52, application or numeric * sequences. */ @SuppressWarnings("unused") private KeypadMode keypadMode; /** * When true, the terminal is in 132-column mode (DECCOLM). */ private boolean columns132 = false; /** * true = reverse video. Set by DECSCNM. */ private boolean reverseVideo = false; /** * false = echo characters locally. */ private boolean fullDuplex = true; /** * The current terminal state. */ private SaveableState currentState; /** * The last saved terminal state. */ private SaveableState savedState; /** * The 88- or 256-color support RGB colors. */ private List colors88; /** * Sixel collection buffer. */ private StringBuilder sixelParseBuffer = new StringBuilder(2048); /** * Sixel shared palette. */ private HashMap 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 userQueue = new ArrayList(); /** * Number of bytes/characters passed to consume(). */ private long readCount = 0; /** * DECSC/DECRC save/restore a subset of the total state. This class * encapsulates those specific flags/modes. */ private class SaveableState { /** * When true, cursor positions are relative to the scrolling region. */ public boolean originMode = false; /** * The current editing X position. */ public int cursorX = 0; /** * 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(); tabStops = new ArrayList(); scrollback = new ArrayList(); display = new ArrayList(); 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(new BufferedInputStream( this.inputStream, 1024 * 128), "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)); } assert (currentState.cursorY < height); assert (currentState.cursorX < width); // 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); // 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]); } } } } // 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 ----------------------------------------------------------------- // ------------------------------------------------------------------------ /** * Wait for a period of time to get output from the launched process. * * @param millis millis to wait for, or 0 to wait forever * @return true if the launched process has emitted something */ public boolean waitForOutput(final int millis) { if (millis < 0) { throw new IllegalArgumentException("timeout must be >= 0"); } int waitedMillis = millis; final int pollTimeout = 5; while (true) { if (readCount != 0) { return true; } if ((millis > 0) && (waitedMillis < 0)){ return false; } try { Thread.sleep(pollTimeout); } catch (InterruptedException e) { // SQUASH } waitedMillis -= pollTimeout; } } /** * 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, with sixel and Jexer image // support. if (!s8c1t) { return "\033[?62;1;6;9;4;22;444c"; } // "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 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; } // 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 getScrollbackBuffer() { return scrollback; } /** * Get the display buffer. * * @return the display buffer */ public final List 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 getVisibleDisplay(final int visibleHeight, final int scrollBottom) { assert (visibleHeight >= 0); assert (scrollBottom >= 0); int visibleBottom = scrollback.size() + display.size() - scrollBottom; List preceedingBlankLines = new ArrayList(); int visibleTop = visibleBottom - visibleHeight; if (visibleTop < 0) { for (int i = visibleTop; i < 0; i++) { preceedingBlankLines.add(getBlankDisplayLine()); } visibleTop = 0; } assert (visibleTop >= 0); List displayLines = new ArrayList(); displayLines.addAll(scrollback); displayLines.addAll(display); List visibleLines = new ArrayList(); 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 copyBuffer(final List buffer) { ArrayList result = new ArrayList(buffer.size()); for (DisplayLine line: buffer) { result.add(new DisplayLine(line)); } return result; } /** * Get the display width. * * @return the width (usually 80 or 132) */ public final int getWidth() { return width; } /** * Set the display width. * * @param width the new width */ public final synchronized void setWidth(final int width) { this.width = width; rightMargin = width - 1; if (currentState.cursorX >= width) { currentState.cursorX = width - 1; } if (savedState.cursorX >= width) { savedState.cursorX = width - 1; } } /** * Get the display height. * * @return the height (usually 24) */ public final int getHeight() { return height; } /** * Set the display height. * * @param height the new height */ public final synchronized void setHeight(final int height) { int delta = height - this.height; this.height = height; scrollRegionBottom += delta; if ((scrollRegionBottom < 0) || (scrollRegionTop > height - 1)) { scrollRegionBottom = height - 1; } if (scrollRegionTop >= scrollRegionBottom) { scrollRegionTop = 0; } if (currentState.cursorY >= height) { currentState.cursorY = height - 1; } if (savedState.cursorY >= height) { savedState.cursorY = height - 1; } while (display.size() < height) { DisplayLine line = new DisplayLine(currentState.attr); line.setReverseColor(reverseVideo); display.add(line); } while (display.size() > height) { appendScrollbackLine(display.remove(0)); } } /** * Get the maximum number of lines in the scrollback buffer. * * @return the maximum number of lines in the scrollback buffer */ public int getScrollbackMax() { return scrollbackMax; } /** * Set the maximum number of lines for the scrollback buffer. * * @param scrollbackMax the maximum number of lines for the scrollback * buffer */ public final void setScrollbackMax(final int scrollbackMax) { this.scrollbackMax = scrollbackMax; } /** * Get visible cursor flag. * * @return if true, the cursor is visible */ public final boolean isCursorVisible() { return cursorVisible; } /** * Get the screen title as set by the xterm OSC sequence. Lots of * applications send a screenTitle regardless of whether it is an xterm * client or not. * * @return screen title */ public final String getScreenTitle() { return screenTitle; } /** * Get 132 columns value. * * @return if true, the terminal is in 132 column mode */ public final boolean isColumns132() { return columns132; } /** * Clear the CSI parameters and flags. */ private void toGround() { csiParams.clear(); collectBuffer.setLength(0); scanState = ScanState.GROUND; } /** * Reset the tab stops list. */ private void resetTabStops() { tabStops.clear(); for (int i = 0; (i * 8) <= rightMargin; i++) { tabStops.add(Integer.valueOf(i * 8)); } } /** * Reset the 88- or 256-colors. */ private void resetColors() { colors88 = new ArrayList(256); for (int i = 0; i < 256; i++) { colors88.add(0); } // Set default system colors. These match DOS colors. colors88.set(0, 0x00000000); colors88.set(1, 0x00a80000); colors88.set(2, 0x0000a800); colors88.set(3, 0x00a85400); colors88.set(4, 0x000000a8); colors88.set(5, 0x00a800a8); colors88.set(6, 0x0000a8a8); colors88.set(7, 0x00a8a8a8); colors88.set(8, 0x00545454); colors88.set(9, 0x00fc5454); colors88.set(10, 0x0054fc54); colors88.set(11, 0x00fcfc54); colors88.set(12, 0x005454fc); colors88.set(13, 0x00fc54fc); colors88.set(14, 0x0054fcfc); colors88.set(15, 0x00fcfcfc); // These match xterm's default colors from 256colres.h. colors88.set(16, 0x000000); colors88.set(17, 0x00005f); colors88.set(18, 0x000087); colors88.set(19, 0x0000af); colors88.set(20, 0x0000d7); colors88.set(21, 0x0000ff); colors88.set(22, 0x005f00); colors88.set(23, 0x005f5f); colors88.set(24, 0x005f87); colors88.set(25, 0x005faf); colors88.set(26, 0x005fd7); colors88.set(27, 0x005fff); colors88.set(28, 0x008700); colors88.set(29, 0x00875f); colors88.set(30, 0x008787); colors88.set(31, 0x0087af); colors88.set(32, 0x0087d7); colors88.set(33, 0x0087ff); colors88.set(34, 0x00af00); colors88.set(35, 0x00af5f); colors88.set(36, 0x00af87); colors88.set(37, 0x00afaf); colors88.set(38, 0x00afd7); colors88.set(39, 0x00afff); colors88.set(40, 0x00d700); colors88.set(41, 0x00d75f); colors88.set(42, 0x00d787); colors88.set(43, 0x00d7af); colors88.set(44, 0x00d7d7); colors88.set(45, 0x00d7ff); colors88.set(46, 0x00ff00); colors88.set(47, 0x00ff5f); colors88.set(48, 0x00ff87); colors88.set(49, 0x00ffaf); colors88.set(50, 0x00ffd7); colors88.set(51, 0x00ffff); colors88.set(52, 0x5f0000); colors88.set(53, 0x5f005f); colors88.set(54, 0x5f0087); colors88.set(55, 0x5f00af); colors88.set(56, 0x5f00d7); colors88.set(57, 0x5f00ff); colors88.set(58, 0x5f5f00); colors88.set(59, 0x5f5f5f); colors88.set(60, 0x5f5f87); colors88.set(61, 0x5f5faf); colors88.set(62, 0x5f5fd7); colors88.set(63, 0x5f5fff); colors88.set(64, 0x5f8700); colors88.set(65, 0x5f875f); colors88.set(66, 0x5f8787); colors88.set(67, 0x5f87af); colors88.set(68, 0x5f87d7); colors88.set(69, 0x5f87ff); colors88.set(70, 0x5faf00); colors88.set(71, 0x5faf5f); colors88.set(72, 0x5faf87); colors88.set(73, 0x5fafaf); colors88.set(74, 0x5fafd7); colors88.set(75, 0x5fafff); colors88.set(76, 0x5fd700); colors88.set(77, 0x5fd75f); colors88.set(78, 0x5fd787); colors88.set(79, 0x5fd7af); colors88.set(80, 0x5fd7d7); colors88.set(81, 0x5fd7ff); colors88.set(82, 0x5fff00); colors88.set(83, 0x5fff5f); colors88.set(84, 0x5fff87); colors88.set(85, 0x5fffaf); colors88.set(86, 0x5fffd7); colors88.set(87, 0x5fffff); colors88.set(88, 0x870000); colors88.set(89, 0x87005f); colors88.set(90, 0x870087); colors88.set(91, 0x8700af); colors88.set(92, 0x8700d7); colors88.set(93, 0x8700ff); colors88.set(94, 0x875f00); colors88.set(95, 0x875f5f); colors88.set(96, 0x875f87); colors88.set(97, 0x875faf); colors88.set(98, 0x875fd7); colors88.set(99, 0x875fff); colors88.set(100, 0x878700); colors88.set(101, 0x87875f); colors88.set(102, 0x878787); colors88.set(103, 0x8787af); colors88.set(104, 0x8787d7); colors88.set(105, 0x8787ff); colors88.set(106, 0x87af00); colors88.set(107, 0x87af5f); colors88.set(108, 0x87af87); colors88.set(109, 0x87afaf); colors88.set(110, 0x87afd7); colors88.set(111, 0x87afff); colors88.set(112, 0x87d700); colors88.set(113, 0x87d75f); colors88.set(114, 0x87d787); colors88.set(115, 0x87d7af); colors88.set(116, 0x87d7d7); colors88.set(117, 0x87d7ff); colors88.set(118, 0x87ff00); colors88.set(119, 0x87ff5f); colors88.set(120, 0x87ff87); colors88.set(121, 0x87ffaf); colors88.set(122, 0x87ffd7); colors88.set(123, 0x87ffff); colors88.set(124, 0xaf0000); colors88.set(125, 0xaf005f); colors88.set(126, 0xaf0087); colors88.set(127, 0xaf00af); colors88.set(128, 0xaf00d7); colors88.set(129, 0xaf00ff); colors88.set(130, 0xaf5f00); colors88.set(131, 0xaf5f5f); colors88.set(132, 0xaf5f87); colors88.set(133, 0xaf5faf); colors88.set(134, 0xaf5fd7); colors88.set(135, 0xaf5fff); colors88.set(136, 0xaf8700); colors88.set(137, 0xaf875f); colors88.set(138, 0xaf8787); colors88.set(139, 0xaf87af); colors88.set(140, 0xaf87d7); colors88.set(141, 0xaf87ff); colors88.set(142, 0xafaf00); colors88.set(143, 0xafaf5f); colors88.set(144, 0xafaf87); colors88.set(145, 0xafafaf); colors88.set(146, 0xafafd7); colors88.set(147, 0xafafff); colors88.set(148, 0xafd700); colors88.set(149, 0xafd75f); colors88.set(150, 0xafd787); colors88.set(151, 0xafd7af); colors88.set(152, 0xafd7d7); colors88.set(153, 0xafd7ff); colors88.set(154, 0xafff00); colors88.set(155, 0xafff5f); colors88.set(156, 0xafff87); colors88.set(157, 0xafffaf); colors88.set(158, 0xafffd7); colors88.set(159, 0xafffff); colors88.set(160, 0xd70000); colors88.set(161, 0xd7005f); colors88.set(162, 0xd70087); colors88.set(163, 0xd700af); colors88.set(164, 0xd700d7); colors88.set(165, 0xd700ff); colors88.set(166, 0xd75f00); colors88.set(167, 0xd75f5f); colors88.set(168, 0xd75f87); colors88.set(169, 0xd75faf); colors88.set(170, 0xd75fd7); colors88.set(171, 0xd75fff); colors88.set(172, 0xd78700); colors88.set(173, 0xd7875f); colors88.set(174, 0xd78787); colors88.set(175, 0xd787af); colors88.set(176, 0xd787d7); colors88.set(177, 0xd787ff); colors88.set(178, 0xd7af00); colors88.set(179, 0xd7af5f); colors88.set(180, 0xd7af87); colors88.set(181, 0xd7afaf); colors88.set(182, 0xd7afd7); colors88.set(183, 0xd7afff); colors88.set(184, 0xd7d700); colors88.set(185, 0xd7d75f); colors88.set(186, 0xd7d787); colors88.set(187, 0xd7d7af); colors88.set(188, 0xd7d7d7); colors88.set(189, 0xd7d7ff); colors88.set(190, 0xd7ff00); colors88.set(191, 0xd7ff5f); colors88.set(192, 0xd7ff87); colors88.set(193, 0xd7ffaf); colors88.set(194, 0xd7ffd7); colors88.set(195, 0xd7ffff); colors88.set(196, 0xff0000); colors88.set(197, 0xff005f); colors88.set(198, 0xff0087); colors88.set(199, 0xff00af); colors88.set(200, 0xff00d7); colors88.set(201, 0xff00ff); colors88.set(202, 0xff5f00); colors88.set(203, 0xff5f5f); colors88.set(204, 0xff5f87); colors88.set(205, 0xff5faf); colors88.set(206, 0xff5fd7); colors88.set(207, 0xff5fff); colors88.set(208, 0xff8700); colors88.set(209, 0xff875f); colors88.set(210, 0xff8787); colors88.set(211, 0xff87af); colors88.set(212, 0xff87d7); colors88.set(213, 0xff87ff); colors88.set(214, 0xffaf00); colors88.set(215, 0xffaf5f); colors88.set(216, 0xffaf87); colors88.set(217, 0xffafaf); colors88.set(218, 0xffafd7); colors88.set(219, 0xffafff); colors88.set(220, 0xffd700); colors88.set(221, 0xffd75f); colors88.set(222, 0xffd787); colors88.set(223, 0xffd7af); colors88.set(224, 0xffd7d7); colors88.set(225, 0xffd7ff); colors88.set(226, 0xffff00); colors88.set(227, 0xffff5f); colors88.set(228, 0xffff87); colors88.set(229, 0xffffaf); colors88.set(230, 0xffffd7); colors88.set(231, 0xffffff); colors88.set(232, 0x080808); colors88.set(233, 0x121212); colors88.set(234, 0x1c1c1c); colors88.set(235, 0x262626); colors88.set(236, 0x303030); colors88.set(237, 0x3a3a3a); colors88.set(238, 0x444444); colors88.set(239, 0x4e4e4e); colors88.set(240, 0x585858); colors88.set(241, 0x626262); colors88.set(242, 0x6c6c6c); colors88.set(243, 0x767676); colors88.set(244, 0x808080); colors88.set(245, 0x8a8a8a); colors88.set(246, 0x949494); colors88.set(247, 0x9e9e9e); colors88.set(248, 0xa8a8a8); colors88.set(249, 0xb2b2b2); colors88.set(250, 0xbcbcbc); colors88.set(251, 0xc6c6c6); colors88.set(252, 0xd0d0d0); colors88.set(253, 0xdadada); colors88.set(254, 0xe4e4e4); colors88.set(255, 0xeeeeee); } /** * Get the RGB value of one of the indexed colors. * * @param index the color index * @return the RGB value */ private int get88Color(final int index) { // System.err.print("get88Color: " + index); if ((index < 0) || (index > colors88.size())) { // System.err.println(" -- UNKNOWN"); return 0; } // System.err.printf(" %08x\n", colors88.get(index)); return colors88.get(index); } /** * Set one of the indexed colors to a color specification. * * @param index the color index * @param spec the specification, typically something like "rgb:aa/bb/cc" */ private void set88Color(final int index, final String spec) { // System.err.println("set88Color: " + index + " '" + spec + "'"); if ((index < 0) || (index > colors88.size())) { return; } if (spec.startsWith("rgb:")) { String [] rgbTokens = spec.substring(4).split("/"); if (rgbTokens.length == 3) { try { int rgb = (Integer.parseInt(rgbTokens[0], 16) << 16); rgb |= Integer.parseInt(rgbTokens[1], 16) << 8; rgb |= Integer.parseInt(rgbTokens[2], 16); // System.err.printf(" set to %08x\n", rgb); colors88.set(index, rgb); } catch (NumberFormatException e) { // SQUASH } } return; } if (spec.toLowerCase().equals("black")) { colors88.set(index, 0x00000000); } else if (spec.toLowerCase().equals("red")) { colors88.set(index, 0x00a80000); } else if (spec.toLowerCase().equals("green")) { colors88.set(index, 0x0000a800); } else if (spec.toLowerCase().equals("yellow")) { colors88.set(index, 0x00a85400); } else if (spec.toLowerCase().equals("blue")) { colors88.set(index, 0x000000a8); } else if (spec.toLowerCase().equals("magenta")) { colors88.set(index, 0x00a800a8); } else if (spec.toLowerCase().equals("cyan")) { colors88.set(index, 0x0000a8a8); } else if (spec.toLowerCase().equals("white")) { colors88.set(index, 0x00a8a8a8); } } /** * Reset the emulation state. */ private void reset() { currentState = new SaveableState(); savedState = new SaveableState(); scanState = ScanState.GROUND; if (displayListener != null) { width = displayListener.getDisplayWidth(); height = displayListener.getDisplayHeight(); } else { width = 80; height = 24; } scrollRegionTop = 0; scrollRegionBottom = height - 1; rightMargin = width - 1; newLineMode = false; arrowKeyMode = ArrowKeyMode.ANSI; keypadMode = KeypadMode.Numeric; wrapLineFlag = false; // Flags shiftOut = false; vt52Mode = false; insertMode = false; columns132 = false; newLineMode = false; reverseVideo = false; fullDuplex = true; cursorVisible = true; // VT220 singleshift = Singleshift.NONE; s8c1t = false; printerControllerMode = false; // XTERM mouseProtocol = MouseProtocol.OFF; mouseEncoding = MouseEncoding.X10; // Tab stops resetTabStops(); // Reset extra colors resetColors(); // Clear CSI stuff toGround(); } /** * Append a to the scrollback buffer, clearing image data for lines more * than three screenfuls in. */ private void appendScrollbackLine(DisplayLine line) { scrollback.add(line); if (scrollback.size() > height * 3) { scrollback.get(scrollback.size() - (height * 3)).clearImages(); } } /** * Append a new line to the bottom of the display, adding lines off the * top to the scrollback buffer. */ private void newDisplayLine() { // Scroll the top line off into the scrollback buffer appendScrollbackLine(display.get(0)); while (scrollback.size() > scrollbackMax) { scrollback.remove(0); scrollback.trimToSize(); } display.remove(0); display.trimToSize(); DisplayLine line = new DisplayLine(currentState.attr); line.setReverseColor(reverseVideo); display.add(line); } /** * Wraps the current line. */ private void wrapCurrentLine() { if (currentState.cursorY == height - 1) { newDisplayLine(); } if (currentState.cursorY < height - 1) { currentState.cursorY++; } currentState.cursorX = 0; } /** * Handle a carriage return. */ private void carriageReturn() { currentState.cursorX = 0; wrapLineFlag = false; } /** * Reverse the color of the visible display. */ private void invertDisplayColors() { for (DisplayLine line: display) { line.setReverseColor(!line.isReverseColor()); } } /** * Handle a linefeed. */ private void linefeed() { if (currentState.cursorY < scrollRegionBottom) { // Increment screen y currentState.cursorY++; } else { // Screen y does not increment /* * Two cases: either we're inside a scrolling region or not. If * the scrolling region bottom is the bottom of the screen, then * push the top line into the buffer. Else scroll the scrolling * region up. */ if ((scrollRegionBottom == height - 1) && (scrollRegionTop == 0)) { // We're at the bottom of the scroll region, AND the scroll // region is the entire screen. // New line newDisplayLine(); } else { // We're at the bottom of the scroll region, AND the scroll // region is NOT the entire screen. scrollingRegionScrollUp(scrollRegionTop, scrollRegionBottom, 1); } } if (newLineMode) { currentState.cursorX = 0; } wrapLineFlag = false; } /** * Prints one character to the display buffer. * * @param ch character to display */ 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()) { rightMargin = ((rightMargin + 1) / 2) - 1; } // Check the unusually-complicated line wrapping conditions... if (currentState.cursorX == rightMargin) { if (currentState.lineWrap == true) { /* * This case happens when: the cursor was already on the * right margin (either through printing or by an explicit * placement command), and a character was printed. * * The line wraps only when a new character arrives AND the * cursor is already on the right margin AND has placed a * character in its cell. Easier to see than to explain. */ if (wrapLineFlag == false) { /* * This block marks the case that we are in the margin * and the first character has been received and printed. */ wrapLineFlag = true; } else { /* * This block marks the case that we are in the margin * and the second character has been received and * printed. */ wrapLineFlag = false; wrapCurrentLine(); } } } else if (currentState.cursorX <= rightMargin) { /* * This is the normal case: a character came in and was printed * to the left of the right margin column. */ // Turn off VT100 special-case flag wrapLineFlag = false; } // "Print" the character Cell newCell = new Cell(ch); CellAttributes newCellAttributes = (CellAttributes) newCell; newCellAttributes.setTo(currentState.attr); DisplayLine line = display.get(currentState.cursorY); 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--; } } } } /** * Translate the mouse event to a VT100, VT220, or XTERM sequence and * send to the remote side. * * @param mouse mouse event received from the local user */ private void mouse(final TMouseEvent mouse) { /* System.err.printf("mouse(): protocol %s encoding %s mouse %s\n", mouseProtocol, mouseEncoding, mouse); */ if (mouseEncoding == MouseEncoding.X10) { // We will support X10 but only for (160,94) and smaller. if ((mouse.getX() >= 160) || (mouse.getY() >= 94)) { return; } } switch (mouseProtocol) { case OFF: // Do nothing return; case X10: // Only report button presses if (mouse.getType() != TMouseEvent.Type.MOUSE_DOWN) { return; } break; case NORMAL: // Only report button presses and releases if ((mouse.getType() != TMouseEvent.Type.MOUSE_DOWN) && (mouse.getType() != TMouseEvent.Type.MOUSE_UP) ) { return; } break; case BUTTONEVENT: /* * Only report button presses, button releases, and motions that * have a button down (i.e. drag-and-drop). */ if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { if (!mouse.isMouse1() && !mouse.isMouse2() && !mouse.isMouse3() && !mouse.isMouseWheelUp() && !mouse.isMouseWheelDown() ) { return; } } break; case ANYEVENT: // Report everything break; } // Now encode the event StringBuilder sb = new StringBuilder(6); if (mouseEncoding == MouseEncoding.SGR) { sb.append((char) 0x1B); sb.append("[<"); int buttons = 0; if (mouse.isMouse1()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { buttons = 32; } else { buttons = 0; } } else if (mouse.isMouse2()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { buttons = 33; } else { buttons = 1; } } else if (mouse.isMouse3()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { buttons = 34; } else { buttons = 2; } } else if (mouse.isMouseWheelUp()) { buttons = 64; } else if (mouse.isMouseWheelDown()) { buttons = 65; } else { // This is motion with no buttons down. buttons = 35; } if (mouse.isAlt()) { buttons |= 0x08; } if (mouse.isCtrl()) { buttons |= 0x10; } if (mouse.isShift()) { buttons |= 0x04; } sb.append(String.format("%d;%d;%d", buttons, mouse.getX() + 1, mouse.getY() + 1)); if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) { sb.append("m"); } else { sb.append("M"); } } else { // X10 and UTF8 encodings sb.append((char) 0x1B); sb.append('['); sb.append('M'); int buttons = 0; if (mouse.getType() == TMouseEvent.Type.MOUSE_UP) { buttons = 0x03 + 32; } else if (mouse.isMouse1()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { buttons = 0x00 + 32 + 32; } else { buttons = 0x00 + 32; } } else if (mouse.isMouse2()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { buttons = 0x01 + 32 + 32; } else { buttons = 0x01 + 32; } } else if (mouse.isMouse3()) { if (mouse.getType() == TMouseEvent.Type.MOUSE_MOTION) { buttons = 0x02 + 32 + 32; } else { buttons = 0x02 + 32; } } else if (mouse.isMouseWheelUp()) { buttons = 0x04 + 64; } else if (mouse.isMouseWheelDown()) { buttons = 0x05 + 64; } else { // This is motion with no buttons down. buttons = 0x03 + 32; } if (mouse.isAlt()) { buttons |= 0x08; } if (mouse.isCtrl()) { buttons |= 0x10; } if (mouse.isShift()) { buttons |= 0x04; } sb.append((char) (buttons & 0xFF)); sb.append((char) (mouse.getX() + 33)); sb.append((char) (mouse.getY() + 33)); } // System.err.printf("Would write: \'%s\'\n", sb.toString()); writeRemote(sb.toString()); } /** * Translate the keyboard press to a VT100, VT220, or XTERM sequence and * send to the remote side. * * @param keypress keypress received from the local user */ private void keypress(final TKeypress keypress) { writeRemote(keypressToString(keypress)); } /** * Build one of the complex xterm keystroke sequences, storing the result in * xterm_keystroke_buffer. * * @param ss3 the prefix to use based on VT100 state. * @param first the first character, usually a number. * @param first the last character, one of the following: ~ A B C D F H * @param ctrl whether or not ctrl is down * @param alt whether or not alt is down * @param shift whether or not shift is down * @return the buffer with the full key sequence */ private String xtermBuildKeySequence(final String ss3, final char first, final char last, boolean ctrl, boolean alt, boolean shift) { StringBuilder sb = new StringBuilder(ss3); if ((last == '~') || (ctrl == true) || (alt == true) || (shift == true) ) { sb.append(first); if ( (ctrl == false) && (alt == false) && (shift == true)) { sb.append(";2"); } else if ((ctrl == false) && (alt == true) && (shift == false)) { sb.append(";3"); } else if ((ctrl == false) && (alt == true) && (shift == true)) { sb.append(";4"); } else if ((ctrl == true) && (alt == false) && (shift == false)) { sb.append(";5"); } else if ((ctrl == true) && (alt == false) && (shift == true)) { sb.append(";6"); } else if ((ctrl == true) && (alt == true) && (shift == false)) { sb.append(";7"); } else if ((ctrl == true) && (alt == true) && (shift == true)) { sb.append(";8"); } } sb.append(last); return sb.toString(); } /** * Translate the keyboard press to a VT100, VT220, or XTERM sequence. * * @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())) { /* * If this is a control character, process it like it came from * the remote side. */ if (keypress.getChar() < 0x20) { handleControlChar((char) keypress.getChar()); } else { // Local echo for everything else printCharacter(keypress.getChar()); } if (displayListener != null) { displayListener.displayChanged(); } } if ((newLineMode == true) && (keypress.equals(kbEnter))) { // NLM: send CRLF return "\015\012"; } // Handle control characters if ((keypress.isCtrl()) && (!keypress.isFnKey())) { StringBuilder sb = new StringBuilder(); int ch = keypress.getChar(); ch -= 0x40; sb.append(Character.toChars(ch)); return sb.toString(); } // Handle alt characters if ((keypress.isAlt()) && (!keypress.isFnKey())) { StringBuilder sb = new StringBuilder("\033"); int ch = keypress.getChar(); sb.append(Character.toChars(ch)); return sb.toString(); } if (keypress.equals(kbBackspaceDel)) { switch (type) { case VT100: return "\010"; case VT102: return "\010"; case VT220: return "\177"; case XTERM: return "\177"; } } if (keypress.equalsWithoutModifiers(kbLeft)) { switch (type) { case XTERM: switch (arrowKeyMode) { case ANSI: return xtermBuildKeySequence("\033[", '1', 'D', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT52: return xtermBuildKeySequence("\033", '1', 'D', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT100: return xtermBuildKeySequence("\033O", '1', 'D', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); } default: switch (arrowKeyMode) { case ANSI: return "\033[D"; case VT52: return "\033D"; case VT100: return "\033OD"; } } } if (keypress.equalsWithoutModifiers(kbRight)) { switch (type) { case XTERM: switch (arrowKeyMode) { case ANSI: return xtermBuildKeySequence("\033[", '1', 'C', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT52: return xtermBuildKeySequence("\033", '1', 'C', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT100: return xtermBuildKeySequence("\033O", '1', 'C', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); } default: switch (arrowKeyMode) { case ANSI: return "\033[C"; case VT52: return "\033C"; case VT100: return "\033OC"; } } } if (keypress.equalsWithoutModifiers(kbUp)) { switch (type) { case XTERM: switch (arrowKeyMode) { case ANSI: return xtermBuildKeySequence("\033[", '1', 'A', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT52: return xtermBuildKeySequence("\033", '1', 'A', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT100: return xtermBuildKeySequence("\033O", '1', 'A', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); } default: switch (arrowKeyMode) { case ANSI: return "\033[A"; case VT52: return "\033A"; case VT100: return "\033OA"; } } } if (keypress.equalsWithoutModifiers(kbDown)) { switch (type) { case XTERM: switch (arrowKeyMode) { case ANSI: return xtermBuildKeySequence("\033[", '1', 'B', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT52: return xtermBuildKeySequence("\033", '1', 'B', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT100: return xtermBuildKeySequence("\033O", '1', 'B', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); } default: switch (arrowKeyMode) { case ANSI: return "\033[B"; case VT52: return "\033B"; case VT100: return "\033OB"; } } } if (keypress.equalsWithoutModifiers(kbHome)) { switch (type) { case XTERM: switch (arrowKeyMode) { case ANSI: return xtermBuildKeySequence("\033[", '1', 'H', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT52: return xtermBuildKeySequence("\033", '1', 'H', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT100: return xtermBuildKeySequence("\033O", '1', 'H', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); } default: switch (arrowKeyMode) { case ANSI: return "\033[H"; case VT52: return "\033H"; case VT100: return "\033OH"; } } } if (keypress.equalsWithoutModifiers(kbEnd)) { switch (type) { case XTERM: switch (arrowKeyMode) { case ANSI: return xtermBuildKeySequence("\033[", '1', 'F', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT52: return xtermBuildKeySequence("\033", '1', 'F', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); case VT100: return xtermBuildKeySequence("\033O", '1', 'F', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); } default: switch (arrowKeyMode) { case ANSI: return "\033[F"; case VT52: return "\033F"; case VT100: return "\033OF"; } } } if (keypress.equals(kbF1)) { // PF1 if (vt52Mode) { return "\033P"; } return "\033OP"; } if (keypress.equals(kbF2)) { // PF2 if (vt52Mode) { return "\033Q"; } return "\033OQ"; } if (keypress.equals(kbF3)) { // PF3 if (vt52Mode) { return "\033R"; } return "\033OR"; } if (keypress.equals(kbF4)) { // PF4 if (vt52Mode) { return "\033S"; } return "\033OS"; } if (keypress.equals(kbF5)) { switch (type) { case VT100: return "\033Ot"; case VT102: return "\033Ot"; case VT220: return "\033[15~"; case XTERM: return "\033[15~"; } } if (keypress.equals(kbF6)) { switch (type) { case VT100: return "\033Ou"; case VT102: return "\033Ou"; case VT220: return "\033[17~"; case XTERM: return "\033[17~"; } } if (keypress.equals(kbF7)) { switch (type) { case VT100: return "\033Ov"; case VT102: return "\033Ov"; case VT220: return "\033[18~"; case XTERM: return "\033[18~"; } } if (keypress.equals(kbF8)) { switch (type) { case VT100: return "\033Ol"; case VT102: return "\033Ol"; case VT220: return "\033[19~"; case XTERM: return "\033[19~"; } } if (keypress.equals(kbF9)) { switch (type) { case VT100: return "\033Ow"; case VT102: return "\033Ow"; case VT220: return "\033[20~"; case XTERM: return "\033[20~"; } } if (keypress.equals(kbF10)) { switch (type) { case VT100: return "\033Ox"; case VT102: return "\033Ox"; case VT220: return "\033[21~"; case XTERM: return "\033[21~"; } } if (keypress.equals(kbF11)) { return "\033[23~"; } if (keypress.equals(kbF12)) { return "\033[24~"; } if (keypress.equals(kbShiftF1)) { // Shifted PF1 if (vt52Mode) { return "\0332P"; } if (type == DeviceType.XTERM) { return "\0331;2P"; } return "\033O2P"; } if (keypress.equals(kbShiftF2)) { // Shifted PF2 if (vt52Mode) { return "\0332Q"; } if (type == DeviceType.XTERM) { return "\0331;2Q"; } return "\033O2Q"; } if (keypress.equals(kbShiftF3)) { // Shifted PF3 if (vt52Mode) { return "\0332R"; } if (type == DeviceType.XTERM) { return "\0331;2R"; } return "\033O2R"; } if (keypress.equals(kbShiftF4)) { // Shifted PF4 if (vt52Mode) { return "\0332S"; } if (type == DeviceType.XTERM) { return "\0331;2S"; } return "\033O2S"; } if (keypress.equals(kbShiftF5)) { // Shifted F5 return "\033[15;2~"; } if (keypress.equals(kbShiftF6)) { // Shifted F6 return "\033[17;2~"; } if (keypress.equals(kbShiftF7)) { // Shifted F7 return "\033[18;2~"; } if (keypress.equals(kbShiftF8)) { // Shifted F8 return "\033[19;2~"; } if (keypress.equals(kbShiftF9)) { // Shifted F9 return "\033[20;2~"; } if (keypress.equals(kbShiftF10)) { // Shifted F10 return "\033[21;2~"; } if (keypress.equals(kbShiftF11)) { // Shifted F11 return "\033[23;2~"; } if (keypress.equals(kbShiftF12)) { // Shifted F12 return "\033[24;2~"; } if (keypress.equals(kbCtrlF1)) { // Control PF1 if (vt52Mode) { return "\0335P"; } if (type == DeviceType.XTERM) { return "\0331;5P"; } return "\033O5P"; } if (keypress.equals(kbCtrlF2)) { // Control PF2 if (vt52Mode) { return "\0335Q"; } if (type == DeviceType.XTERM) { return "\0331;5Q"; } return "\033O5Q"; } if (keypress.equals(kbCtrlF3)) { // Control PF3 if (vt52Mode) { return "\0335R"; } if (type == DeviceType.XTERM) { return "\0331;5R"; } return "\033O5R"; } if (keypress.equals(kbCtrlF4)) { // Control PF4 if (vt52Mode) { return "\0335S"; } if (type == DeviceType.XTERM) { return "\0331;5S"; } return "\033O5S"; } if (keypress.equals(kbCtrlF5)) { // Control F5 return "\033[15;5~"; } if (keypress.equals(kbCtrlF6)) { // Control F6 return "\033[17;5~"; } if (keypress.equals(kbCtrlF7)) { // Control F7 return "\033[18;5~"; } if (keypress.equals(kbCtrlF8)) { // Control F8 return "\033[19;5~"; } if (keypress.equals(kbCtrlF9)) { // Control F9 return "\033[20;5~"; } if (keypress.equals(kbCtrlF10)) { // Control F10 return "\033[21;5~"; } if (keypress.equals(kbCtrlF11)) { // Control F11 return "\033[23;5~"; } if (keypress.equals(kbCtrlF12)) { // Control F12 return "\033[24;5~"; } if (keypress.equalsWithoutModifiers(kbPgUp)) { switch (type) { case XTERM: return xtermBuildKeySequence("\033[", '5', '~', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); default: return "\033[5~"; } } if (keypress.equalsWithoutModifiers(kbPgDn)) { switch (type) { case XTERM: return xtermBuildKeySequence("\033[", '6', '~', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); default: return "\033[6~"; } } if (keypress.equalsWithoutModifiers(kbIns)) { switch (type) { case XTERM: return xtermBuildKeySequence("\033[", '2', '~', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); default: return "\033[2~"; } } if (keypress.equalsWithoutModifiers(kbDel)) { switch (type) { case XTERM: return xtermBuildKeySequence("\033[", '3', '~', keypress.isCtrl(), keypress.isAlt(), keypress.isShift()); default: // Delete sends real delete for VTxxx return "\177"; } } if (keypress.equals(kbEnter)) { return "\015"; } if (keypress.equals(kbEsc)) { return "\033"; } if (keypress.equals(kbAltEsc)) { return "\033\033"; } if (keypress.equals(kbTab)) { return "\011"; } if ((keypress.equalsWithoutModifiers(kbBackTab)) || (keypress.equals(kbShiftTab)) ) { switch (type) { case XTERM: return "\033[Z"; default: return "\011"; } } // Non-alt, non-ctrl characters if (!keypress.isFnKey()) { StringBuilder sb = new StringBuilder(); sb.append(Character.toChars(keypress.getChar())); return sb.toString(); } return ""; } /** * Map a symbol in any one of the VT100/VT220 character sets to a Unicode * symbol. * * @param ch 8-bit character from the remote side * @param charsetGl character set defined for GL * @param charsetGr character set defined for GR * @return character to display on the screen */ private char mapCharacterCharset(final int ch, final CharacterSet charsetGl, final CharacterSet charsetGr) { int lookupChar = ch; CharacterSet lookupCharset = charsetGl; if (ch >= 0x80) { assert ((type == DeviceType.VT220) || (type == DeviceType.XTERM)); lookupCharset = charsetGr; lookupChar &= 0x7F; } switch (lookupCharset) { case DRAWING: return DECCharacterSets.SPECIAL_GRAPHICS[lookupChar]; case UK: return DECCharacterSets.UK[lookupChar]; case US: return DECCharacterSets.US_ASCII[lookupChar]; case NRC_DUTCH: return DECCharacterSets.NL[lookupChar]; case NRC_FINNISH: return DECCharacterSets.FI[lookupChar]; case NRC_FRENCH: return DECCharacterSets.FR[lookupChar]; case NRC_FRENCH_CA: return DECCharacterSets.FR_CA[lookupChar]; case NRC_GERMAN: return DECCharacterSets.DE[lookupChar]; case NRC_ITALIAN: return DECCharacterSets.IT[lookupChar]; case NRC_NORWEGIAN: return DECCharacterSets.NO[lookupChar]; case NRC_SPANISH: return DECCharacterSets.ES[lookupChar]; case NRC_SWEDISH: return DECCharacterSets.SV[lookupChar]; case NRC_SWISS: return DECCharacterSets.SWISS[lookupChar]; case DEC_SUPPLEMENTAL: return DECCharacterSets.DEC_SUPPLEMENTAL[lookupChar]; case VT52_GRAPHICS: return DECCharacterSets.VT52_SPECIAL_GRAPHICS[lookupChar]; case ROM: return DECCharacterSets.US_ASCII[lookupChar]; case ROM_SPECIAL: return DECCharacterSets.US_ASCII[lookupChar]; default: throw new IllegalArgumentException("Invalid character set value: " + lookupCharset); } } /** * Map an 8-bit byte into a printable character. * * @param ch either 8-bit or Unicode character from the remote side * @return character to display on the screen */ private int mapCharacter(final int ch) { if (ch >= 0x100) { // Unicode character, just return it return ch; } CharacterSet charsetGl = currentState.g0Charset; CharacterSet charsetGr = currentState.grCharset; if (vt52Mode == true) { if (shiftOut == true) { // Shifted out character, pull from VT52 graphics charsetGl = currentState.g1Charset; charsetGr = CharacterSet.US; } else { // Normal charsetGl = currentState.g0Charset; charsetGr = CharacterSet.US; } // Pull the character return mapCharacterCharset(ch, charsetGl, charsetGr); } // shiftOout if (shiftOut == true) { // Shifted out character, pull from G1 charsetGl = currentState.g1Charset; charsetGr = currentState.grCharset; // Pull the character return mapCharacterCharset(ch, charsetGl, charsetGr); } // SS2 if (singleshift == Singleshift.SS2) { singleshift = Singleshift.NONE; // Shifted out character, pull from G2 charsetGl = currentState.g2Charset; charsetGr = currentState.grCharset; } // SS3 if (singleshift == Singleshift.SS3) { singleshift = Singleshift.NONE; // Shifted out character, pull from G3 charsetGl = currentState.g3Charset; charsetGr = currentState.grCharset; } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // Check for locking shift switch (currentState.glLockshift) { case G1_GR: throw new IllegalArgumentException("programming bug"); case G2_GR: throw new IllegalArgumentException("programming bug"); case G3_GR: throw new IllegalArgumentException("programming bug"); case G2_GL: // LS2 charsetGl = currentState.g2Charset; break; case G3_GL: // LS3 charsetGl = currentState.g3Charset; break; case NONE: // Normal charsetGl = currentState.g0Charset; break; } switch (currentState.grLockshift) { case G2_GL: throw new IllegalArgumentException("programming bug"); case G3_GL: throw new IllegalArgumentException("programming bug"); case G1_GR: // LS1R charsetGr = currentState.g1Charset; break; case G2_GR: // LS2R charsetGr = currentState.g2Charset; break; case G3_GR: // LS3R charsetGr = currentState.g3Charset; break; case NONE: // Normal charsetGr = CharacterSet.DEC_SUPPLEMENTAL; break; } } // Pull the character return mapCharacterCharset(ch, charsetGl, charsetGr); } /** * Scroll the text within a scrolling region up n lines. * * @param regionTop top row of the scrolling region * @param regionBottom bottom row of the scrolling region * @param n number of lines to scroll */ private void scrollingRegionScrollUp(final int regionTop, final int regionBottom, final int n) { if (regionTop >= regionBottom) { return; } // Sanity check: see if there will be any characters left after the // scroll if (regionBottom + 1 - regionTop <= n) { // There won't be anything left in the region, so just call // eraseScreen() and return. eraseScreen(regionTop, 0, regionBottom, width - 1, false); return; } int remaining = regionBottom + 1 - regionTop - n; List displayTop = display.subList(0, regionTop); List displayBottom = display.subList(regionBottom + 1, display.size()); List displayMiddle = display.subList(regionBottom + 1 - remaining, regionBottom + 1); display = new ArrayList(displayTop); display.addAll(displayMiddle); for (int i = 0; i < n; i++) { DisplayLine line = new DisplayLine(currentState.attr); line.setReverseColor(reverseVideo); display.add(line); } display.addAll(displayBottom); assert (display.size() == height); } /** * Scroll the text within a scrolling region down n lines. * * @param regionTop top row of the scrolling region * @param regionBottom bottom row of the scrolling region * @param n number of lines to scroll */ private void scrollingRegionScrollDown(final int regionTop, final int regionBottom, final int n) { if (regionTop >= regionBottom) { return; } // Sanity check: see if there will be any characters left after the // scroll if (regionBottom + 1 - regionTop <= n) { // There won't be anything left in the region, so just call // eraseScreen() and return. eraseScreen(regionTop, 0, regionBottom, width - 1, false); return; } int remaining = regionBottom + 1 - regionTop - n; List displayTop = display.subList(0, regionTop); List displayBottom = display.subList(regionBottom + 1, display.size()); List displayMiddle = display.subList(regionTop, regionTop + remaining); display = new ArrayList(displayTop); for (int i = 0; i < n; i++) { DisplayLine line = new DisplayLine(currentState.attr); line.setReverseColor(reverseVideo); display.add(line); } display.addAll(displayMiddle); display.addAll(displayBottom); assert (display.size() == height); } /** * Process a control character. * * @param ch 8-bit character from the remote side */ private void handleControlChar(final char ch) { assert ((ch <= 0x1F) || ((ch >= 0x7F) && (ch <= 0x9F))); switch (ch) { case 0x00: // NUL - discard return; case 0x05: // ENQ // Transmit the answerback message. // Not supported break; case 0x07: // BEL // Not supported break; case 0x08: // BS cursorLeft(1, false); break; case 0x09: // HT advanceToNextTabStop(); break; case 0x0A: // LF linefeed(); break; case 0x0B: // VT linefeed(); break; case 0x0C: // FF linefeed(); break; case 0x0D: // CR carriageReturn(); break; case 0x0E: // SO shiftOut = true; currentState.glLockshift = LockshiftMode.NONE; break; case 0x0F: // SI shiftOut = false; currentState.glLockshift = LockshiftMode.NONE; break; case 0x84: // IND ind(); break; case 0x85: // NEL nel(); break; case 0x88: // HTS hts(); break; case 0x8D: // RI ri(); break; case 0x8E: // SS2 singleshift = Singleshift.SS2; break; case 0x8F: // SS3 singleshift = Singleshift.SS3; break; default: break; } } /** * Advance the cursor to the next tab stop. */ private void advanceToNextTabStop() { if (tabStops.size() == 0) { // Go to the rightmost column cursorRight(rightMargin - currentState.cursorX, false); return; } for (Integer stop: tabStops) { if (stop > currentState.cursorX) { cursorRight(stop - currentState.cursorX, false); return; } } /* * We got here, meaning there isn't a tab stop beyond the current * cursor position. Place the cursor of the right-most edge of the * screen. */ cursorRight(rightMargin - currentState.cursorX, false); } /** * Save a character into the collect buffer. * * @param ch character to save */ private void collect(final char ch) { collectBuffer.append(ch); } /** * Save a byte into the CSI parameters buffer. * * @param ch byte to save */ private void param(final byte ch) { if (csiParams.size() == 0) { csiParams.add(Integer.valueOf(0)); } Integer x = csiParams.get(csiParams.size() - 1); if ((ch >= '0') && (ch <= '9')) { x *= 10; x += (ch - '0'); csiParams.set(csiParams.size() - 1, x); } if ((ch == ';') && (csiParams.size() < 16)) { csiParams.add(Integer.valueOf(0)); } } /** * Get a CSI parameter value, with a default. * * @param position parameter index. 0 is the first parameter. * @param defaultValue value to use if csiParams[position] doesn't exist * @return parameter value */ private int getCsiParam(final int position, final int defaultValue) { if (csiParams.size() < position + 1) { return defaultValue; } return csiParams.get(position).intValue(); } /** * Get a CSI parameter value, clamped to within min/max. * * @param position parameter index. 0 is the first parameter. * @param defaultValue value to use if csiParams[position] doesn't exist * @param minValue minimum value inclusive * @param maxValue maximum value inclusive * @return parameter value */ private int getCsiParam(final int position, final int defaultValue, final int minValue, final int maxValue) { assert (minValue <= maxValue); int value = getCsiParam(position, defaultValue); if (value < minValue) { value = minValue; } if (value > maxValue) { value = maxValue; } return value; } /** * Set or unset a toggle. * * @param value true for set ('h'), false for reset ('l') */ private void setToggle(final boolean value) { boolean decPrivateModeFlag = false; for (int i = 0; i < collectBuffer.length(); i++) { if (collectBuffer.charAt(i) == '?') { decPrivateModeFlag = true; break; } } for (Integer i: csiParams) { switch (i) { case 1: if (decPrivateModeFlag == true) { // DECCKM if (value == true) { // Use application arrow keys arrowKeyMode = ArrowKeyMode.VT100; } else { // Use ANSI arrow keys arrowKeyMode = ArrowKeyMode.ANSI; } } break; case 2: if (decPrivateModeFlag == true) { if (value == false) { // DECANM vt52Mode = true; arrowKeyMode = ArrowKeyMode.VT52; /* * From the VT102 docs: "You use ANSI mode to select * most terminal features; the terminal uses the same * features when it switches to VT52 mode. You * cannot, however, change most of these features in * VT52 mode." * * In other words, do not reset any other attributes * when switching between VT52 submode and ANSI. * * HOWEVER, the real vt100 does switch the character * set according to Usenet. */ currentState.g0Charset = CharacterSet.US; currentState.g1Charset = CharacterSet.DRAWING; shiftOut = false; if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // VT52 mode is explicitly 7-bit s8c1t = false; singleshift = Singleshift.NONE; } } } else { // KAM if (value == true) { // Turn off keyboard // Not supported } else { // Turn on keyboard // Not supported } } break; case 3: if (decPrivateModeFlag == true) { // DECCOLM if (value == true) { // 132 columns columns132 = true; rightMargin = 131; } else { // 80 columns columns132 = false; if ((displayListener != null) && (type == DeviceType.XTERM) ) { // For xterms, reset to the actual width, not 80 // columns. width = displayListener.getDisplayWidth(); rightMargin = width - 1; } else { rightMargin = 79; width = rightMargin + 1; } } // Entire screen is cleared, and scrolling region is // reset eraseScreen(0, 0, height - 1, width - 1, false); scrollRegionTop = 0; scrollRegionBottom = height - 1; // Also home the cursor cursorPosition(0, 0); } break; case 4: if (decPrivateModeFlag == true) { // DECSCLM if (value == true) { // Smooth scroll // Not supported } else { // Jump scroll // Not supported } } else { // IRM if (value == true) { insertMode = true; } else { insertMode = false; } } break; case 5: if (decPrivateModeFlag == true) { // DECSCNM if (value == true) { /* * Set selects reverse screen, a white screen * background with black characters. */ if (reverseVideo != true) { /* * If in normal video, switch it back */ invertDisplayColors(); } reverseVideo = true; } else { /* * Reset selects normal screen, a black screen * background with white characters. */ if (reverseVideo == true) { /* * If in reverse video already, switch it back */ invertDisplayColors(); } reverseVideo = false; } } break; case 6: if (decPrivateModeFlag == true) { // DECOM if (value == true) { // Origin is relative to scroll region cursor. // Cursor can NEVER leave scrolling region. currentState.originMode = true; cursorPosition(0, 0); } else { // Origin is absolute to entire screen. Cursor can // leave the scrolling region via cup() and hvp(). currentState.originMode = false; cursorPosition(0, 0); } } break; case 7: if (decPrivateModeFlag == true) { // DECAWM if (value == true) { // Turn linewrap on currentState.lineWrap = true; } else { // Turn linewrap off currentState.lineWrap = false; } } break; case 8: if (decPrivateModeFlag == true) { // DECARM if (value == true) { // Keyboard auto-repeat on // Not supported } else { // Keyboard auto-repeat off // Not supported } } break; case 12: if (decPrivateModeFlag == false) { // SRM if (value == true) { // Local echo off fullDuplex = true; } else { // Local echo on fullDuplex = false; } } break; case 18: if (decPrivateModeFlag == true) { // DECPFF // Not supported } break; case 19: if (decPrivateModeFlag == true) { // DECPEX // Not supported } break; case 20: if (decPrivateModeFlag == false) { // LNM if (value == true) { /* * Set causes a received linefeed, form feed, or * vertical tab to move cursor to first column of * next line. RETURN transmits both a carriage return * and linefeed. This selection is also called new * line option. */ newLineMode = true; } else { /* * Reset causes a received linefeed, form feed, or * vertical tab to move cursor to next line in * current column. RETURN transmits a carriage * return. */ newLineMode = false; } } break; case 25: if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if (decPrivateModeFlag == true) { // DECTCEM if (value == true) { // Visible cursor cursorVisible = true; } else { // Invisible cursor cursorVisible = false; } } } break; case 42: if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if (decPrivateModeFlag == true) { // DECNRCM if (value == true) { // Select national mode NRC // Not supported } else { // Select multi-national mode // Not supported } } } break; case 80: if (type == DeviceType.XTERM) { if (decPrivateModeFlag == true) { if (value == true) { // Enable sixel scrolling (default). // Not supported } else { // Disable sixel scrolling. // Not supported } } } break; case 1000: if ((type == DeviceType.XTERM) && (decPrivateModeFlag == true) ) { // Mouse: normal tracking mode if (value == true) { mouseProtocol = MouseProtocol.NORMAL; } else { mouseProtocol = MouseProtocol.OFF; } } break; case 1002: if ((type == DeviceType.XTERM) && (decPrivateModeFlag == true) ) { // Mouse: normal tracking mode if (value == true) { mouseProtocol = MouseProtocol.BUTTONEVENT; } else { mouseProtocol = MouseProtocol.OFF; } } break; case 1003: if ((type == DeviceType.XTERM) && (decPrivateModeFlag == true) ) { // Mouse: Any-event tracking mode if (value == true) { mouseProtocol = MouseProtocol.ANYEVENT; } else { mouseProtocol = MouseProtocol.OFF; } } break; case 1005: if ((type == DeviceType.XTERM) && (decPrivateModeFlag == true) ) { // Mouse: UTF-8 coordinates if (value == true) { mouseEncoding = MouseEncoding.UTF8; } else { mouseEncoding = MouseEncoding.X10; } } break; case 1006: if ((type == DeviceType.XTERM) && (decPrivateModeFlag == true) ) { // Mouse: SGR coordinates if (value == true) { mouseEncoding = MouseEncoding.SGR; } else { mouseEncoding = MouseEncoding.X10; } } 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(); } } } break; default: break; } } } /** * DECSC - Save cursor. */ private void decsc() { savedState.setTo(currentState); } /** * DECRC - Restore cursor. */ private void decrc() { currentState.setTo(savedState); } /** * IND - Index. */ private void ind() { // Move the cursor and scroll if necessary. If at the bottom line // already, a scroll up is supposed to be performed. if (currentState.cursorY == scrollRegionBottom) { scrollingRegionScrollUp(scrollRegionTop, scrollRegionBottom, 1); } cursorDown(1, true); } /** * RI - Reverse index. */ private void ri() { // Move the cursor and scroll if necessary. If at the top line // already, a scroll down is supposed to be performed. if (currentState.cursorY == scrollRegionTop) { scrollingRegionScrollDown(scrollRegionTop, scrollRegionBottom, 1); } cursorUp(1, true); } /** * NEL - Next line. */ private void nel() { // Move the cursor and scroll if necessary. If at the bottom line // already, a scroll up is supposed to be performed. if (currentState.cursorY == scrollRegionBottom) { scrollingRegionScrollUp(scrollRegionTop, scrollRegionBottom, 1); } cursorDown(1, true); // Reset to the beginning of the next line currentState.cursorX = 0; } /** * DECKPAM - Keypad application mode. */ private void deckpam() { keypadMode = KeypadMode.Application; } /** * DECKPNM - Keypad numeric mode. */ private void deckpnm() { keypadMode = KeypadMode.Numeric; } /** * Move up n spaces. * * @param n number of spaces to move * @param honorScrollRegion if true, then do nothing if the cursor is * outside the scrolling region */ private void cursorUp(final int n, final boolean honorScrollRegion) { int top; /* * Special case: if a user moves the cursor from the right margin, we * have to reset the VT100 right margin flag. */ if (n > 0) { wrapLineFlag = false; } for (int i = 0; i < n; i++) { if (honorScrollRegion == true) { // Honor the scrolling region if ((currentState.cursorY < scrollRegionTop) || (currentState.cursorY > scrollRegionBottom) ) { // Outside region, do nothing return; } // Inside region, go up top = scrollRegionTop; } else { // Non-scrolling case top = 0; } if (currentState.cursorY > top) { currentState.cursorY--; } } } /** * Move down n spaces. * * @param n number of spaces to move * @param honorScrollRegion if true, then do nothing if the cursor is * outside the scrolling region */ private void cursorDown(final int n, final boolean honorScrollRegion) { int bottom; /* * Special case: if a user moves the cursor from the right margin, we * have to reset the VT100 right margin flag. */ if (n > 0) { wrapLineFlag = false; } for (int i = 0; i < n; i++) { if (honorScrollRegion == true) { // Honor the scrolling region if (currentState.cursorY > scrollRegionBottom) { // Outside region, do nothing return; } // Inside region, go down bottom = scrollRegionBottom; } else { // Non-scrolling case bottom = height - 1; } if (currentState.cursorY < bottom) { currentState.cursorY++; } } } /** * Move left n spaces. * * @param n number of spaces to move * @param honorScrollRegion if true, then do nothing if the cursor is * outside the scrolling region */ private void cursorLeft(final int n, final boolean honorScrollRegion) { /* * Special case: if a user moves the cursor from the right margin, we * have to reset the VT100 right margin flag. */ if (n > 0) { wrapLineFlag = false; } for (int i = 0; i < n; i++) { if (honorScrollRegion == true) { // Honor the scrolling region if ((currentState.cursorY < scrollRegionTop) || (currentState.cursorY > scrollRegionBottom) ) { // Outside region, do nothing return; } } if (currentState.cursorX > 0) { currentState.cursorX--; } } } /** * Move right n spaces. * * @param n number of spaces to move * @param honorScrollRegion if true, then do nothing if the cursor is * outside the scrolling region */ private void cursorRight(final int n, final boolean honorScrollRegion) { int rightMargin = this.rightMargin; /* * Special case: if a user moves the cursor from the right margin, we * have to reset the VT100 right margin flag. */ if (n > 0) { wrapLineFlag = false; } if (display.get(currentState.cursorY).isDoubleWidth()) { rightMargin = ((rightMargin + 1) / 2) - 1; } for (int i = 0; i < n; i++) { if (honorScrollRegion == true) { // Honor the scrolling region if ((currentState.cursorY < scrollRegionTop) || (currentState.cursorY > scrollRegionBottom) ) { // Outside region, do nothing return; } } if (currentState.cursorX < rightMargin) { currentState.cursorX++; } } } /** * Move cursor to (col, row) where (0, 0) is the top-left corner. * * @param row row to move to * @param col column to move to */ private void cursorPosition(int row, final int col) { int rightMargin = this.rightMargin; assert (col >= 0); assert (row >= 0); if (display.get(currentState.cursorY).isDoubleWidth()) { rightMargin = ((rightMargin + 1) / 2) - 1; } // Set column number currentState.cursorX = col; // Sanity check, bring column back to margin. if (currentState.cursorX > rightMargin) { currentState.cursorX = rightMargin; } // Set row number if (currentState.originMode == true) { row += scrollRegionTop; } if (currentState.cursorY < row) { cursorDown(row - currentState.cursorY, false); } else if (currentState.cursorY > row) { cursorUp(currentState.cursorY - row, false); } wrapLineFlag = false; } /** * HTS - Horizontal tabulation set. */ private void hts() { for (Integer stop: tabStops) { if (stop == currentState.cursorX) { // Already have a tab stop here return; } } // Append a tab stop to the end of the array and resort them tabStops.add(currentState.cursorX); Collections.sort(tabStops); } /** * DECSWL - Single-width line. */ private void decswl() { display.get(currentState.cursorY).setDoubleWidth(false); display.get(currentState.cursorY).setDoubleHeight(0); } /** * DECDWL - Double-width line. */ private void decdwl() { display.get(currentState.cursorY).setDoubleWidth(true); display.get(currentState.cursorY).setDoubleHeight(0); } /** * DECHDL - Double-height + double-width line. * * @param topHalf if true, this sets the row to be the top half row of a * double-height row */ private void dechdl(final boolean topHalf) { display.get(currentState.cursorY).setDoubleWidth(true); if (topHalf == true) { display.get(currentState.cursorY).setDoubleHeight(1); } else { display.get(currentState.cursorY).setDoubleHeight(2); } } /** * DECALN - Screen alignment display. */ private void decaln() { Cell newCell = new Cell('E'); for (DisplayLine line: display) { for (int i = 0; i < line.length(); i++) { line.replace(i, newCell); } } } /** * DECSCL - Compatibility level. */ private void decscl() { int i = getCsiParam(0, 0); int j = getCsiParam(1, 0); if (i == 61) { // Reset fonts currentState.g0Charset = CharacterSet.US; currentState.g1Charset = CharacterSet.DRAWING; s8c1t = false; } else if (i == 62) { if ((j == 0) || (j == 2)) { s8c1t = true; } else if (j == 1) { s8c1t = false; } } } /** * CUD - Cursor down. */ private void cud() { cursorDown(getCsiParam(0, 1, 1, height), true); } /** * CUF - Cursor forward. */ private void cuf() { cursorRight(getCsiParam(0, 1, 1, rightMargin + 1), true); } /** * CUB - Cursor backward. */ private void cub() { cursorLeft(getCsiParam(0, 1, 1, currentState.cursorX + 1), true); } /** * CUU - Cursor up. */ private void cuu() { cursorUp(getCsiParam(0, 1, 1, currentState.cursorY + 1), true); } /** * CUP - Cursor position. */ private void cup() { cursorPosition(getCsiParam(0, 1, 1, height) - 1, getCsiParam(1, 1, 1, rightMargin + 1) - 1); } /** * CNL - Cursor down and to column 1. */ private void cnl() { cursorDown(getCsiParam(0, 1, 1, height), true); // To column 0 cursorLeft(currentState.cursorX, true); } /** * CPL - Cursor up and to column 1. */ private void cpl() { cursorUp(getCsiParam(0, 1, 1, currentState.cursorY + 1), true); // To column 0 cursorLeft(currentState.cursorX, true); } /** * CHA - Cursor to column # in current row. */ private void cha() { cursorPosition(currentState.cursorY, getCsiParam(0, 1, 1, rightMargin + 1) - 1); } /** * VPA - Cursor to row #, same column. */ private void vpa() { cursorPosition(getCsiParam(0, 1, 1, height) - 1, currentState.cursorX); } /** * ED - Erase in display. */ private void ed() { boolean honorProtected = false; boolean decPrivateModeFlag = false; for (int i = 0; i < collectBuffer.length(); i++) { if (collectBuffer.charAt(i) == '?') { decPrivateModeFlag = true; break; } } if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (decPrivateModeFlag == true) ) { honorProtected = true; } int i = getCsiParam(0, 0); if (i == 0) { // Erase from here to end of screen if (currentState.cursorY < height - 1) { eraseScreen(currentState.cursorY + 1, 0, height - 1, width - 1, honorProtected); } eraseLine(currentState.cursorX, width - 1, honorProtected); } else if (i == 1) { // Erase from beginning of screen to here eraseScreen(0, 0, currentState.cursorY - 1, width - 1, honorProtected); eraseLine(0, currentState.cursorX, honorProtected); } else if (i == 2) { // Erase entire screen eraseScreen(0, 0, height - 1, width - 1, honorProtected); } } /** * EL - Erase in line. */ private void el() { boolean honorProtected = false; boolean decPrivateModeFlag = false; for (int i = 0; i < collectBuffer.length(); i++) { if (collectBuffer.charAt(i) == '?') { decPrivateModeFlag = true; break; } } if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (decPrivateModeFlag == true) ) { honorProtected = true; } int i = getCsiParam(0, 0); if (i == 0) { // Erase from here to end of line eraseLine(currentState.cursorX, width - 1, honorProtected); } else if (i == 1) { // Erase from beginning of line to here eraseLine(0, currentState.cursorX, honorProtected); } else if (i == 2) { // Erase entire line eraseLine(0, width - 1, honorProtected); } } /** * ECH - Erase # of characters in current row. */ private void ech() { int i = getCsiParam(0, 1, 1, width); // Erase from here to i characters eraseLine(currentState.cursorX, currentState.cursorX + i - 1, false); } /** * IL - Insert line. */ private void il() { int i = getCsiParam(0, 1); if ((currentState.cursorY >= scrollRegionTop) && (currentState.cursorY <= scrollRegionBottom) ) { // I can get the same effect with a scroll-down scrollingRegionScrollDown(currentState.cursorY, scrollRegionBottom, i); } } /** * DCH - Delete char. */ private void dch() { int n = getCsiParam(0, 1); DisplayLine line = display.get(currentState.cursorY); Cell blank = new Cell(); for (int i = 0; i < n; i++) { line.delete(currentState.cursorX, blank); } } /** * ICH - Insert blank char at cursor. */ private void ich() { int n = getCsiParam(0, 1); DisplayLine line = display.get(currentState.cursorY); Cell blank = new Cell(); for (int i = 0; i < n; i++) { line.insert(currentState.cursorX, blank); } } /** * DL - Delete line. */ private void dl() { int i = getCsiParam(0, 1); if ((currentState.cursorY >= scrollRegionTop) && (currentState.cursorY <= scrollRegionBottom)) { // I can get the same effect with a scroll-down scrollingRegionScrollUp(currentState.cursorY, scrollRegionBottom, i); } } /** * HVP - Horizontal and vertical position. */ private void hvp() { cup(); } /** * REP - Repeat character. */ private void rep() { int n = getCsiParam(0, 1); for (int i = 0; i < n; i++) { printCharacter(repCh); } } /** * SU - Scroll up. */ private void su() { scrollingRegionScrollUp(scrollRegionTop, scrollRegionBottom, getCsiParam(0, 1, 1, height)); } /** * SD - Scroll down. */ private void sd() { scrollingRegionScrollDown(scrollRegionTop, scrollRegionBottom, getCsiParam(0, 1, 1, height)); } /** * CBT - Go back X tab stops. */ private void cbt() { int tabsToMove = getCsiParam(0, 1); int tabI; for (int i = 0; i < tabsToMove; i++) { int j = currentState.cursorX; for (tabI = 0; tabI < tabStops.size(); tabI++) { if (tabStops.get(tabI) >= currentState.cursorX) { break; } } tabI--; if (tabI <= 0) { j = 0; } else { j = tabStops.get(tabI); } cursorPosition(currentState.cursorY, j); } } /** * CHT - Advance X tab stops. */ private void cht() { int n = getCsiParam(0, 1); for (int i = 0; i < n; i++) { advanceToNextTabStop(); } } /** * SGR - Select graphics rendition. */ private void sgr() { if (csiParams.size() == 0) { currentState.attr.reset(); return; } int sgrColorMode = -1; boolean idx88Color = false; boolean rgbColor = false; int rgbRed = -1; int rgbGreen = -1; for (Integer i: csiParams) { if ((sgrColorMode == 38) || (sgrColorMode == 48)) { assert (type == DeviceType.XTERM); if (idx88Color) { /* * Indexed color mode, we now have the index number. */ if (sgrColorMode == 38) { currentState.attr.setForeColorRGB(get88Color(i)); } else { assert (sgrColorMode == 48); currentState.attr.setBackColorRGB(get88Color(i)); } sgrColorMode = -1; idx88Color = false; continue; } if (rgbColor) { /* * RGB color mode, we are collecting tokens. */ if (rgbRed == -1) { rgbRed = i & 0xFF; } else if (rgbGreen == -1) { rgbGreen = i & 0xFF; } else { int rgb = rgbRed << 16; rgb |= rgbGreen << 8; rgb |= i & 0xFF; // System.err.printf("RGB: %08x\n", rgb); if (sgrColorMode == 38) { currentState.attr.setForeColorRGB(rgb); } else { assert (sgrColorMode == 48); currentState.attr.setBackColorRGB(rgb); } rgbRed = -1; rgbGreen = -1; sgrColorMode = -1; rgbColor = false; } continue; } switch (i) { case 2: /* * RGB color mode. */ rgbColor = true; continue; case 5: /* * Indexed color mode. */ idx88Color = true; continue; default: /* * This is neither indexed nor RGB color. Bail out. */ return; } } // if ((sgrColorMode == 38) || (sgrColorMode == 48)) switch (i) { case 0: // Normal currentState.attr.reset(); break; case 1: // Bold currentState.attr.setBold(true); break; case 4: // Underline currentState.attr.setUnderline(true); break; case 5: // Blink currentState.attr.setBlink(true); break; case 7: // Reverse currentState.attr.setReverse(true); break; default: break; } if (type == DeviceType.XTERM) { switch (i) { case 8: // Invisible // Not supported break; case 90: // Set black foreground currentState.attr.setForeColorRGB(get88Color(8)); break; case 91: // Set red foreground currentState.attr.setForeColorRGB(get88Color(9)); break; case 92: // Set green foreground currentState.attr.setForeColorRGB(get88Color(10)); break; case 93: // Set yellow foreground currentState.attr.setForeColorRGB(get88Color(11)); break; case 94: // Set blue foreground currentState.attr.setForeColorRGB(get88Color(12)); break; case 95: // Set magenta foreground currentState.attr.setForeColorRGB(get88Color(13)); break; case 96: // Set cyan foreground currentState.attr.setForeColorRGB(get88Color(14)); break; case 97: // Set white foreground currentState.attr.setForeColorRGB(get88Color(15)); break; case 100: // Set black background currentState.attr.setBackColorRGB(get88Color(8)); break; case 101: // Set red background currentState.attr.setBackColorRGB(get88Color(9)); break; case 102: // Set green background currentState.attr.setBackColorRGB(get88Color(10)); break; case 103: // Set yellow background currentState.attr.setBackColorRGB(get88Color(11)); break; case 104: // Set blue background currentState.attr.setBackColorRGB(get88Color(12)); break; case 105: // Set magenta background currentState.attr.setBackColorRGB(get88Color(13)); break; case 106: // Set cyan background currentState.attr.setBackColorRGB(get88Color(14)); break; case 107: // Set white background currentState.attr.setBackColorRGB(get88Color(15)); break; default: break; } } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { switch (i) { case 22: // Normal intensity currentState.attr.setBold(false); break; case 24: // No underline currentState.attr.setUnderline(false); break; case 25: // No blink currentState.attr.setBlink(false); break; case 27: // Un-reverse currentState.attr.setReverse(false); break; default: break; } } // A true VT100/102/220 does not support color, however everyone // is used to their terminal emulator supporting color so we will // unconditionally support color for all DeviceType's. switch (i) { case 30: // Set black foreground currentState.attr.setForeColor(Color.BLACK); currentState.attr.setForeColorRGB(-1); break; case 31: // Set red foreground currentState.attr.setForeColor(Color.RED); currentState.attr.setForeColorRGB(-1); break; case 32: // Set green foreground currentState.attr.setForeColor(Color.GREEN); currentState.attr.setForeColorRGB(-1); break; case 33: // Set yellow foreground currentState.attr.setForeColor(Color.YELLOW); currentState.attr.setForeColorRGB(-1); break; case 34: // Set blue foreground currentState.attr.setForeColor(Color.BLUE); currentState.attr.setForeColorRGB(-1); break; case 35: // Set magenta foreground currentState.attr.setForeColor(Color.MAGENTA); currentState.attr.setForeColorRGB(-1); break; case 36: // Set cyan foreground currentState.attr.setForeColor(Color.CYAN); currentState.attr.setForeColorRGB(-1); break; case 37: // Set white foreground currentState.attr.setForeColor(Color.WHITE); currentState.attr.setForeColorRGB(-1); break; case 38: if (type == DeviceType.XTERM) { /* * Xterm supports T.416 / ISO-8613-3 codes to select * either an indexed color or an RGB value. (It also * permits these ISO-8613-3 SGR sequences to be separated * by colons rather than semicolons.) * * We will support only the following: * * 1. Indexed color mode (88- or 256-color modes). * * 2. Direct RGB. * * These cover most of the use cases in the real world. * * HOWEVER, note that this is an awful broken "standard", * with no way to do it "right". See * http://invisible-island.net/ncurses/ncurses.faq.html#xterm_16MegaColors * for a detailed discussion of the current state of RGB * in various terminals, the point of which is that none * of them really do the same thing despite all appearing * to be "xterm". * * Also see * https://bugs.kde.org/show_bug.cgi?id=107487#c3 . * where it is assumed that supporting just the "indexed * mode" of these sequences (which could align easily * with existing SGR colors) is assumed to mean full * support of 24-bit RGB. So it is all or nothing. * * Finally, these sequences break the assumptions of * standard ECMA-48 style parsers as pointed out at * https://bugs.kde.org/show_bug.cgi?id=107487#c11 . * Therefore in order to keep a clean display, we cannot * parse anything else in this sequence. */ sgrColorMode = 38; continue; } else { // Underscore on, default foreground color currentState.attr.setUnderline(true); currentState.attr.setForeColor(Color.WHITE); } break; case 39: // Underscore off, default foreground color currentState.attr.setUnderline(false); currentState.attr.setForeColor(Color.WHITE); currentState.attr.setForeColorRGB(-1); break; case 40: // Set black background currentState.attr.setBackColor(Color.BLACK); currentState.attr.setBackColorRGB(-1); break; case 41: // Set red background currentState.attr.setBackColor(Color.RED); currentState.attr.setBackColorRGB(-1); break; case 42: // Set green background currentState.attr.setBackColor(Color.GREEN); currentState.attr.setBackColorRGB(-1); break; case 43: // Set yellow background currentState.attr.setBackColor(Color.YELLOW); currentState.attr.setBackColorRGB(-1); break; case 44: // Set blue background currentState.attr.setBackColor(Color.BLUE); currentState.attr.setBackColorRGB(-1); break; case 45: // Set magenta background currentState.attr.setBackColor(Color.MAGENTA); currentState.attr.setBackColorRGB(-1); break; case 46: // Set cyan background currentState.attr.setBackColor(Color.CYAN); currentState.attr.setBackColorRGB(-1); break; case 47: // Set white background currentState.attr.setBackColor(Color.WHITE); currentState.attr.setBackColorRGB(-1); break; case 48: if (type == DeviceType.XTERM) { /* * Xterm supports T.416 / ISO-8613-3 codes to select * either an indexed color or an RGB value. (It also * permits these ISO-8613-3 SGR sequences to be separated * by colons rather than semicolons.) * * We will support only the following: * * 1. Indexed color mode (88- or 256-color modes). * * 2. Direct RGB. * * These cover most of the use cases in the real world. * * HOWEVER, note that this is an awful broken "standard", * with no way to do it "right". See * http://invisible-island.net/ncurses/ncurses.faq.html#xterm_16MegaColors * for a detailed discussion of the current state of RGB * in various terminals, the point of which is that none * of them really do the same thing despite all appearing * to be "xterm". * * Also see * https://bugs.kde.org/show_bug.cgi?id=107487#c3 . * where it is assumed that supporting just the "indexed * mode" of these sequences (which could align easily * with existing SGR colors) is assumed to mean full * support of 24-bit RGB. So it is all or nothing. * * Finally, these sequences break the assumptions of * standard ECMA-48 style parsers as pointed out at * https://bugs.kde.org/show_bug.cgi?id=107487#c11 . * Therefore in order to keep a clean display, we cannot * parse anything else in this sequence. */ sgrColorMode = 48; continue; } break; case 49: // Default background currentState.attr.setBackColor(Color.BLACK); currentState.attr.setBackColorRGB(-1); break; default: break; } } } /** * DA - Device attributes. */ private void da() { int extendedFlag = 0; int i = 0; if (collectBuffer.length() > 0) { String args = collectBuffer.substring(1); if (collectBuffer.charAt(0) == '>') { extendedFlag = 1; if (collectBuffer.length() >= 2) { i = Integer.parseInt(args); } } else if (collectBuffer.charAt(0) == '=') { extendedFlag = 2; if (collectBuffer.length() >= 2) { i = Integer.parseInt(args); } } else { // Unknown code, bail out return; } } if ((i != 0) && (i != 1)) { return; } if ((extendedFlag == 0) && (i == 0)) { // Send string directly to remote side writeRemote(deviceTypeResponse()); return; } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((extendedFlag == 1) && (i == 0)) { /* * Request "What type of terminal are you, what is your * firmware version, and what hardware options do you have * installed?" * * Respond: "I am a VT220 (identification code of 1), my * firmware version is _____ (Pv), and I have _____ Po * options installed." * * (Same as xterm) * */ if (s8c1t == true) { writeRemote("\u009b>1;10;0c"); } else { writeRemote("\033[>1;10;0c"); } } } // VT420 and up if ((extendedFlag == 2) && (i == 0)) { /* * Request "What is your unit ID?" * * Respond: "I was manufactured at site 00 and have a unique ID * number of 123." * */ writeRemote("\033P!|00010203\033\\"); } } /** * DECSTBM - Set top and bottom margins. */ private void decstbm() { boolean decPrivateModeFlag = false; for (int i = 0; i < collectBuffer.length(); i++) { if (collectBuffer.charAt(i) == '?') { decPrivateModeFlag = true; break; } } if (decPrivateModeFlag) { // This could be restore DEC private mode values. // Ignore it. } else { // 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; } scrollRegionTop = top; scrollRegionBottom = bottom; // Home cursor cursorPosition(0, 0); } } /** * DECREQTPARM - Request terminal parameters. */ private void decreqtparm() { int i = getCsiParam(0, 0); if ((i != 0) && (i != 1)) { return; } String str = ""; /* * Request terminal parameters. * * Respond with: * * Parity NONE, 8 bits, xmitspeed 38400, recvspeed 38400. * (CLoCk MULtiplier = 1, STP option flags = 0) * * (Same as xterm) */ if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (s8c1t == true) ) { str = String.format("\u009b%d;1;1;128;128;1;0x", i + 2); } else { str = String.format("\033[%d;1;1;128;128;1;0x", i + 2); } writeRemote(str); } /** * DECSCA - Select Character Attributes. */ private void decsca() { int i = getCsiParam(0, 0); if ((i == 0) || (i == 2)) { // Protect mode OFF currentState.attr.setProtect(false); } if (i == 1) { // Protect mode ON currentState.attr.setProtect(true); } } /** * DECSTR - Soft Terminal Reset. */ private void decstr() { // Do exactly like RIS - Reset to initial state reset(); // Do I clear screen too? I think so... eraseScreen(0, 0, height - 1, width - 1, false); cursorPosition(0, 0); } /** * DSR - Device status report. */ private void dsr() { boolean decPrivateModeFlag = false; int row = currentState.cursorY; for (int i = 0; i < collectBuffer.length(); i++) { if (collectBuffer.charAt(i) == '?') { decPrivateModeFlag = true; break; } } int i = getCsiParam(0, 0); switch (i) { case 5: // Request status report. Respond with "OK, no malfunction." // Send string directly to remote side if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (s8c1t == true) ) { writeRemote("\u009b0n"); } else { writeRemote("\033[0n"); } break; case 6: // Request cursor position. Respond with current position. if (currentState.originMode == true) { row -= scrollRegionTop; } String str = ""; if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (s8c1t == true) ) { str = String.format("\u009b%d;%dR", row + 1, currentState.cursorX + 1); } else { str = String.format("\033[%d;%dR", row + 1, currentState.cursorX + 1); } // Send string directly to remote side writeRemote(str); break; case 15: if (decPrivateModeFlag == true) { // Request printer status report. Respond with "Printer not // connected." if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (s8c1t == true)) { writeRemote("\u009b?13n"); } else { writeRemote("\033[?13n"); } } break; case 25: if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (decPrivateModeFlag == true) ) { // Request user-defined keys are locked or unlocked. Respond // with "User-defined keys are locked." if (s8c1t == true) { writeRemote("\u009b?21n"); } else { writeRemote("\033[?21n"); } } break; case 26: if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (decPrivateModeFlag == true) ) { // Request keyboard language. Respond with "Keyboard // language is North American." if (s8c1t == true) { writeRemote("\u009b?27;1n"); } else { writeRemote("\033[?27;1n"); } } break; default: // Some other option, ignore break; } } /** * TBC - Tabulation clear. */ private void tbc() { int i = getCsiParam(0, 0); if (i == 0) { List newStops = new ArrayList(); for (Integer stop: tabStops) { if (stop == currentState.cursorX) { continue; } newStops.add(stop); } tabStops = newStops; } if (i == 3) { tabStops.clear(); } } /** * Erase the characters in the current line from the start column to the * end column, inclusive. * * @param start starting column to erase (between 0 and width - 1) * @param end ending column to erase (between 0 and width - 1) * @param honorProtected if true, do not erase characters with the * protected attribute set */ private void eraseLine(int start, int end, final boolean honorProtected) { if (start > end) { return; } if (end > width - 1) { end = width - 1; } if (start < 0) { start = 0; } for (int i = start; i <= end; i++) { DisplayLine line = display.get(currentState.cursorY); if ((!honorProtected) || ((honorProtected) && (!line.charAt(i).isProtect()))) { switch (type) { case VT100: case VT102: case VT220: /* * From the VT102 manual: * * Erasing a character also erases any character * attribute of the character. */ line.setBlank(i); break; case XTERM: /* * Erase with the current color a.k.a. back-color erase * (bce). */ line.setChar(i, ' '); line.setAttr(i, currentState.attr); break; } } } } /** * Erase a rectangular section of the screen, inclusive. end column, * inclusive. * * @param startRow starting row to erase (between 0 and height - 1) * @param startCol starting column to erase (between 0 and width - 1) * @param endRow ending row to erase (between 0 and height - 1) * @param endCol ending column to erase (between 0 and width - 1) * @param honorProtected if true, do not erase characters with the * protected attribute set */ private void eraseScreen(final int startRow, final int startCol, final int endRow, final int endCol, final boolean honorProtected) { int oldCursorY; if ((startRow < 0) || (startCol < 0) || (endRow < 0) || (endCol < 0) || (endRow < startRow) || (endCol < startCol) ) { return; } oldCursorY = currentState.cursorY; for (int i = startRow; i <= endRow; i++) { currentState.cursorY = i; eraseLine(startCol, endCol, honorProtected); // Erase display clears the double attributes display.get(i).setDoubleWidth(false); display.get(i).setDoubleHeight(0); } currentState.cursorY = oldCursorY; } /** * VT220 printer functions. All of these are parsed, but won't do * anything. */ private void printerFunctions() { boolean decPrivateModeFlag = false; for (int i = 0; i < collectBuffer.length(); i++) { if (collectBuffer.charAt(i) == '?') { decPrivateModeFlag = true; break; } } int i = getCsiParam(0, 0); switch (i) { case 0: if (decPrivateModeFlag == false) { // Print screen } break; case 1: if (decPrivateModeFlag == true) { // Print cursor line } break; case 4: if (decPrivateModeFlag == true) { // Auto print mode OFF } else { // Printer controller OFF // Characters re-appear on the screen printerControllerMode = false; } break; case 5: if (decPrivateModeFlag == true) { // Auto print mode } else { // Printer controller // Characters get sucked into oblivion printerControllerMode = true; } break; default: break; } } /** * Handle the SCAN_OSC_STRING state. Handle this in VT100 because lots * of remote systems will send an XTerm title sequence even if TERM isn't * xterm. * * @param xtermChar the character received from the remote side */ private void oscPut(final char xtermChar) { // System.err.println("oscPut: " + xtermChar); boolean oscEnd = false; if (xtermChar == 0x07) { oscEnd = true; } if ((xtermChar == '\\') && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033') ) { oscEnd = true; } // Collect first collectBuffer.append(xtermChar); // Xterm cases... if (oscEnd) { String args = null; if (xtermChar == 0x07) { args = collectBuffer.substring(0, collectBuffer.length() - 1); } else { args = collectBuffer.substring(0, collectBuffer.length() - 2); } String [] p = args.split(";"); if (p.length > 0) { if ((p[0].equals("0")) || (p[0].equals("2"))) { if (p.length > 1) { // Screen title screenTitle = p[1]; } } if (p[0].equals("4")) { for (int i = 1; i + 1 < p.length; i += 2) { // Set a color index value try { set88Color(Integer.parseInt(p[i]), p[i + 1]); } catch (NumberFormatException e) { // SQUASH } } } 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 toGround(); return; } } /** * 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); boolean pmEnd = false; if ((pmChar == '\\') && (collectBuffer.charAt(collectBuffer.length() - 1) == '\033') ) { pmEnd = true; } // Collect first collectBuffer.append(pmChar); // Xterm cases... if (pmEnd) { 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(final int ch) { readCount++; // DEBUG // System.err.printf("%c STATE = %s\n", ch, scanState); // Special "anywhere" states // 18, 1A --> execute, then switch to SCAN_GROUND if ((ch == 0x18) || (ch == 0x1A)) { // CAN and SUB abort escape sequences toGround(); return; } // 80-8F, 91-97, 99, 9A, 9C --> execute, then switch to SCAN_GROUND // 0x1B == ESCAPE if (ch == 0x1B) { if ((type == DeviceType.XTERM) && ((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; } } // 0x9B == CSI 8-bit sequence if (ch == 0x9B) { scanState = ScanState.CSI_ENTRY; return; } // 0x9D goes to ScanState.OSC_STRING if (ch == 0x9D) { scanState = ScanState.OSC_STRING; return; } // 0x90 goes to DCS_ENTRY if (ch == 0x90) { scanState = ScanState.DCS_ENTRY; return; } // 0x98, 0x9E, and 0x9F go to SOSPMAPC_STRING if ((ch == 0x98) || (ch == 0x9E) || (ch == 0x9F)) { scanState = ScanState.SOSPMAPC_STRING; return; } // 0x7F (DEL) is always discarded if (ch == 0x7F) { return; } switch (scanState) { case GROUND: // 00-17, 19, 1C-1F --> execute // 80-8F, 91-9A, 9C --> execute if ((ch <= 0x1F) || ((ch >= 0x80) && (ch <= 0x9F))) { handleControlChar((char) ch); } // 20-7F --> print if (((ch >= 0x20) && (ch <= 0x7F)) || (ch >= 0xA0) ) { // VT220 printer --> trash bin if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (printerControllerMode == true) ) { return; } // Hang onto this character repCh = mapCharacter(ch); // Print this character printCharacter(repCh); } return; case ESCAPE: // 00-17, 19, 1C-1F --> execute if (ch <= 0x1F) { handleControlChar((char) ch); return; } // 20-2F --> collect, then switch to ESCAPE_INTERMEDIATE if ((ch >= 0x20) && (ch <= 0x2F)) { collect((char) ch); scanState = ScanState.ESCAPE_INTERMEDIATE; return; } // 30-4F, 51-57, 59, 5A, 5C, 60-7E --> dispatch, then switch to GROUND if ((ch >= 0x30) && (ch <= 0x4F)) { switch (ch) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': break; case '7': // DECSC - Save cursor // Note this code overlaps both ANSI and VT52 mode decsc(); break; case '8': // DECRC - Restore cursor // Note this code overlaps both ANSI and VT52 mode decrc(); break; case '9': case ':': case ';': break; case '<': if (vt52Mode == true) { // DECANM - Enter ANSI mode vt52Mode = false; arrowKeyMode = ArrowKeyMode.VT100; /* * From the VT102 docs: "You use ANSI mode to select * most terminal features; the terminal uses the same * features when it switches to VT52 mode. You * cannot, however, change most of these features in * VT52 mode." * * In other words, do not reset any other attributes * when switching between VT52 submode and ANSI. */ // Reset fonts currentState.g0Charset = CharacterSet.US; currentState.g1Charset = CharacterSet.DRAWING; s8c1t = false; singleshift = Singleshift.NONE; currentState.glLockshift = LockshiftMode.NONE; currentState.grLockshift = LockshiftMode.NONE; } break; case '=': // DECKPAM - Keypad application mode // Note this code overlaps both ANSI and VT52 mode deckpam(); break; case '>': // DECKPNM - Keypad numeric mode // Note this code overlaps both ANSI and VT52 mode deckpnm(); break; case '?': case '@': break; case 'A': if (vt52Mode == true) { // Cursor up, and stop at the top without scrolling cursorUp(1, false); } break; case 'B': if (vt52Mode == true) { // Cursor down, and stop at the bottom without scrolling cursorDown(1, false); } break; case 'C': if (vt52Mode == true) { // Cursor right, and stop at the right without scrolling cursorRight(1, false); } break; case 'D': if (vt52Mode == true) { // Cursor left, and stop at the left without scrolling cursorLeft(1, false); } else { // IND - Index ind(); } break; case 'E': if (vt52Mode == true) { // Nothing } else { // NEL - Next line nel(); } break; case 'F': if (vt52Mode == true) { // G0 --> Special graphics currentState.g0Charset = CharacterSet.VT52_GRAPHICS; } break; case 'G': if (vt52Mode == true) { // G0 --> ASCII set currentState.g0Charset = CharacterSet.US; } break; case 'H': if (vt52Mode == true) { // Cursor to home cursorPosition(0, 0); } else { // HTS - Horizontal tabulation set hts(); } break; case 'I': if (vt52Mode == true) { // Reverse line feed. Same as RI. ri(); } break; case 'J': if (vt52Mode == true) { // Erase to end of screen eraseLine(currentState.cursorX, width - 1, false); eraseScreen(currentState.cursorY + 1, 0, height - 1, width - 1, false); } break; case 'K': if (vt52Mode == true) { // Erase to end of line eraseLine(currentState.cursorX, width - 1, false); } break; case 'L': break; case 'M': if (vt52Mode == true) { // Nothing } else { // RI - Reverse index ri(); } break; case 'N': if (vt52Mode == false) { // SS2 singleshift = Singleshift.SS2; } break; case 'O': if (vt52Mode == false) { // SS3 singleshift = Singleshift.SS3; } break; } toGround(); return; } if ((ch >= 0x51) && (ch <= 0x57)) { switch (ch) { case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': break; } toGround(); return; } if (ch == 0x59) { // 'Y' if (vt52Mode == true) { scanState = ScanState.VT52_DIRECT_CURSOR_ADDRESS; } else { toGround(); } return; } if (ch == 0x5A) { // 'Z' if (vt52Mode == true) { // Identify // Send string directly to remote side writeRemote("\033/Z"); } else { // DECID // Send string directly to remote side writeRemote(deviceTypeResponse()); } toGround(); return; } if (ch == 0x5C) { // '\' toGround(); return; } // VT52 cannot get to any of these other states if (vt52Mode == true) { toGround(); return; } if ((ch >= 0x60) && (ch <= 0x7E)) { switch (ch) { case '`': case 'a': case 'b': break; case 'c': // RIS - Reset to initial state reset(); // Do I clear screen too? I think so... eraseScreen(0, 0, height - 1, width - 1, false); cursorPosition(0, 0); break; case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': break; case 'n': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // VT220 lockshift G2 into GL currentState.glLockshift = LockshiftMode.G2_GL; shiftOut = false; } break; case 'o': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // VT220 lockshift G3 into GL currentState.glLockshift = LockshiftMode.G3_GL; shiftOut = false; } break; case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': break; case '|': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // VT220 lockshift G3 into GR currentState.grLockshift = LockshiftMode.G3_GR; shiftOut = false; } break; case '}': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // VT220 lockshift G2 into GR currentState.grLockshift = LockshiftMode.G2_GR; shiftOut = false; } break; case '~': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // VT220 lockshift G1 into GR currentState.grLockshift = LockshiftMode.G1_GR; shiftOut = false; } break; } toGround(); } // 7F --> ignore // 0x5B goes to CSI_ENTRY if (ch == 0x5B) { scanState = ScanState.CSI_ENTRY; } // 0x5D goes to OSC_STRING if (ch == 0x5D) { scanState = ScanState.OSC_STRING; } // 0x50 goes to DCS_ENTRY if (ch == 0x50) { scanState = ScanState.DCS_ENTRY; } // 0x58, 0x5E, and 0x5F go to SOSPMAPC_STRING if ((ch == 0x58) || (ch == 0x5E) || (ch == 0x5F)) { scanState = ScanState.SOSPMAPC_STRING; } return; case ESCAPE_INTERMEDIATE: // 00-17, 19, 1C-1F --> execute if (ch <= 0x1F) { handleControlChar((char) ch); } // 20-2F --> collect if ((ch >= 0x20) && (ch <= 0x2F)) { collect((char) ch); } // 30-7E --> dispatch, then switch to GROUND if ((ch >= 0x30) && (ch <= 0x7E)) { switch (ch) { case '0': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> Special graphics currentState.g0Charset = CharacterSet.DRAWING; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> Special graphics currentState.g1Charset = CharacterSet.DRAWING; } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> Special graphics currentState.g2Charset = CharacterSet.DRAWING; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> Special graphics currentState.g3Charset = CharacterSet.DRAWING; } } break; case '1': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> Alternate character ROM standard character set currentState.g0Charset = CharacterSet.ROM; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> Alternate character ROM standard character set currentState.g1Charset = CharacterSet.ROM; } break; case '2': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> Alternate character ROM special graphics currentState.g0Charset = CharacterSet.ROM_SPECIAL; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> Alternate character ROM special graphics currentState.g1Charset = CharacterSet.ROM_SPECIAL; } break; case '3': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '#')) { // DECDHL - Double-height line (top half) dechdl(true); } break; case '4': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '#')) { // DECDHL - Double-height line (bottom half) dechdl(false); } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> DUTCH currentState.g0Charset = CharacterSet.NRC_DUTCH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> DUTCH currentState.g1Charset = CharacterSet.NRC_DUTCH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> DUTCH currentState.g2Charset = CharacterSet.NRC_DUTCH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> DUTCH currentState.g3Charset = CharacterSet.NRC_DUTCH; } } break; case '5': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '#')) { // DECSWL - Single-width line decswl(); } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> FINNISH currentState.g0Charset = CharacterSet.NRC_FINNISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> FINNISH currentState.g1Charset = CharacterSet.NRC_FINNISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> FINNISH currentState.g2Charset = CharacterSet.NRC_FINNISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> FINNISH currentState.g3Charset = CharacterSet.NRC_FINNISH; } } break; case '6': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '#')) { // DECDWL - Double-width line decdwl(); } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> NORWEGIAN currentState.g0Charset = CharacterSet.NRC_NORWEGIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> NORWEGIAN currentState.g1Charset = CharacterSet.NRC_NORWEGIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> NORWEGIAN currentState.g2Charset = CharacterSet.NRC_NORWEGIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> NORWEGIAN currentState.g3Charset = CharacterSet.NRC_NORWEGIAN; } } break; case '7': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> SWEDISH currentState.g0Charset = CharacterSet.NRC_SWEDISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> SWEDISH currentState.g1Charset = CharacterSet.NRC_SWEDISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> SWEDISH currentState.g2Charset = CharacterSet.NRC_SWEDISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> SWEDISH currentState.g3Charset = CharacterSet.NRC_SWEDISH; } } break; case '8': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '#')) { // DECALN - Screen alignment display decaln(); } break; case '9': case ':': case ';': break; case '<': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> DEC_SUPPLEMENTAL currentState.g0Charset = CharacterSet.DEC_SUPPLEMENTAL; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> DEC_SUPPLEMENTAL currentState.g1Charset = CharacterSet.DEC_SUPPLEMENTAL; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> DEC_SUPPLEMENTAL currentState.g2Charset = CharacterSet.DEC_SUPPLEMENTAL; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> DEC_SUPPLEMENTAL currentState.g3Charset = CharacterSet.DEC_SUPPLEMENTAL; } } break; case '=': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> SWISS currentState.g0Charset = CharacterSet.NRC_SWISS; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> SWISS currentState.g1Charset = CharacterSet.NRC_SWISS; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> SWISS currentState.g2Charset = CharacterSet.NRC_SWISS; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> SWISS currentState.g3Charset = CharacterSet.NRC_SWISS; } } break; case '>': case '?': case '@': break; case 'A': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> United Kingdom set currentState.g0Charset = CharacterSet.UK; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> United Kingdom set currentState.g1Charset = CharacterSet.UK; } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> United Kingdom set currentState.g2Charset = CharacterSet.UK; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> United Kingdom set currentState.g3Charset = CharacterSet.UK; } } break; case 'B': if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> ASCII set currentState.g0Charset = CharacterSet.US; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> ASCII set currentState.g1Charset = CharacterSet.US; } if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> ASCII currentState.g2Charset = CharacterSet.US; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> ASCII currentState.g3Charset = CharacterSet.US; } } break; case 'C': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> FINNISH currentState.g0Charset = CharacterSet.NRC_FINNISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> FINNISH currentState.g1Charset = CharacterSet.NRC_FINNISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> FINNISH currentState.g2Charset = CharacterSet.NRC_FINNISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> FINNISH currentState.g3Charset = CharacterSet.NRC_FINNISH; } } break; case 'D': break; case 'E': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> NORWEGIAN currentState.g0Charset = CharacterSet.NRC_NORWEGIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> NORWEGIAN currentState.g1Charset = CharacterSet.NRC_NORWEGIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> NORWEGIAN currentState.g2Charset = CharacterSet.NRC_NORWEGIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> NORWEGIAN currentState.g3Charset = CharacterSet.NRC_NORWEGIAN; } } break; case 'F': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ' ')) { // S7C1T s8c1t = false; } } break; case 'G': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ' ')) { // S8C1T s8c1t = true; } } break; case 'H': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> SWEDISH currentState.g0Charset = CharacterSet.NRC_SWEDISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> SWEDISH currentState.g1Charset = CharacterSet.NRC_SWEDISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> SWEDISH currentState.g2Charset = CharacterSet.NRC_SWEDISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> SWEDISH currentState.g3Charset = CharacterSet.NRC_SWEDISH; } } break; case 'I': case 'J': break; case 'K': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> GERMAN currentState.g0Charset = CharacterSet.NRC_GERMAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> GERMAN currentState.g1Charset = CharacterSet.NRC_GERMAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> GERMAN currentState.g2Charset = CharacterSet.NRC_GERMAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> GERMAN currentState.g3Charset = CharacterSet.NRC_GERMAN; } } break; case 'L': case 'M': case 'N': case 'O': case 'P': break; case 'Q': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> FRENCH_CA currentState.g0Charset = CharacterSet.NRC_FRENCH_CA; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> FRENCH_CA currentState.g1Charset = CharacterSet.NRC_FRENCH_CA; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> FRENCH_CA currentState.g2Charset = CharacterSet.NRC_FRENCH_CA; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> FRENCH_CA currentState.g3Charset = CharacterSet.NRC_FRENCH_CA; } } break; case 'R': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> FRENCH currentState.g0Charset = CharacterSet.NRC_FRENCH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> FRENCH currentState.g1Charset = CharacterSet.NRC_FRENCH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> FRENCH currentState.g2Charset = CharacterSet.NRC_FRENCH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> FRENCH currentState.g3Charset = CharacterSet.NRC_FRENCH; } } break; case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': break; case 'Y': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> ITALIAN currentState.g0Charset = CharacterSet.NRC_ITALIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> ITALIAN currentState.g1Charset = CharacterSet.NRC_ITALIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> ITALIAN currentState.g2Charset = CharacterSet.NRC_ITALIAN; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> ITALIAN currentState.g3Charset = CharacterSet.NRC_ITALIAN; } } break; case 'Z': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '(')) { // G0 --> SPANISH currentState.g0Charset = CharacterSet.NRC_SPANISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == ')')) { // G1 --> SPANISH currentState.g1Charset = CharacterSet.NRC_SPANISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '*')) { // G2 --> SPANISH currentState.g2Charset = CharacterSet.NRC_SPANISH; } if ((collectBuffer.length() == 1) && (collectBuffer.charAt(0) == '+')) { // G3 --> SPANISH currentState.g3Charset = CharacterSet.NRC_SPANISH; } } break; case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': case 'p': case 'q': case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': break; } toGround(); } // 7F --> ignore // 0x9C goes to GROUND if (ch == 0x9C) { toGround(); } return; case CSI_ENTRY: // 00-17, 19, 1C-1F --> execute if (ch <= 0x1F) { handleControlChar((char) ch); } // 20-2F --> collect, then switch to CSI_INTERMEDIATE if ((ch >= 0x20) && (ch <= 0x2F)) { collect((char) ch); scanState = ScanState.CSI_INTERMEDIATE; } // 30-39, 3B --> param, then switch to CSI_PARAM if ((ch >= '0') && (ch <= '9')) { param((byte) ch); scanState = ScanState.CSI_PARAM; } if (ch == ';') { param((byte) ch); scanState = ScanState.CSI_PARAM; } // 3C-3F --> collect, then switch to CSI_PARAM if ((ch >= 0x3C) && (ch <= 0x3F)) { collect((char) ch); scanState = ScanState.CSI_PARAM; } // 40-7E --> dispatch, then switch to GROUND if ((ch >= 0x40) && (ch <= 0x7E)) { switch (ch) { case '@': // ICH - Insert character ich(); break; case 'A': // CUU - Cursor up cuu(); break; case 'B': // CUD - Cursor down cud(); break; case 'C': // CUF - Cursor forward cuf(); break; case 'D': // CUB - Cursor backward cub(); break; case 'E': // CNL - Cursor down and to column 1 if (type == DeviceType.XTERM) { cnl(); } break; case 'F': // CPL - Cursor up and to column 1 if (type == DeviceType.XTERM) { cpl(); } break; case 'G': // CHA - Cursor to column # in current row if (type == DeviceType.XTERM) { cha(); } break; case 'H': // CUP - Cursor position cup(); break; case 'I': // CHT - Cursor forward X tab stops (default 1) if (type == DeviceType.XTERM) { cht(); } break; case 'J': // ED - Erase in display ed(); break; case 'K': // EL - Erase in line el(); break; case 'L': // IL - Insert line il(); break; case 'M': // DL - Delete line dl(); break; case 'N': case 'O': break; case 'P': // DCH - Delete character dch(); break; case 'Q': case 'R': break; case 'S': // Scroll up X lines (default 1) if (type == DeviceType.XTERM) { 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': // Scroll down X lines (default 1) if (type == DeviceType.XTERM) { sd(); } break; case 'U': case 'V': case 'W': break; case 'X': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // ECH - Erase character ech(); } break; case 'Y': break; case 'Z': // CBT - Cursor backward X tab stops (default 1) if (type == DeviceType.XTERM) { cbt(); } break; case '[': case '\\': case ']': case '^': case '_': break; case '`': // HPA - Cursor to column # in current row. Same as CHA if (type == DeviceType.XTERM) { cha(); } break; case 'a': // HPR - Cursor right. Same as CUF if (type == DeviceType.XTERM) { cuf(); } break; case 'b': // REP - Repeat last char X times if (type == DeviceType.XTERM) { rep(); } break; case 'c': // DA - Device attributes da(); break; case 'd': // VPA - Cursor to row, current column. if (type == DeviceType.XTERM) { vpa(); } break; case 'e': // VPR - Cursor down. Same as CUD if (type == DeviceType.XTERM) { cud(); } break; case 'f': // HVP - Horizontal and vertical position hvp(); break; case 'g': // TBC - Tabulation clear tbc(); break; case 'h': // Sets an ANSI or DEC private toggle setToggle(true); break; case 'i': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // Printer functions printerFunctions(); } break; case 'j': case 'k': break; case 'l': // Sets an ANSI or DEC private toggle setToggle(false); break; case 'm': // SGR - Select graphics rendition sgr(); break; case 'n': // DSR - Device status report dsr(); break; case 'o': case 'p': break; case 'q': // DECLL - Load leds // Not supported break; case 'r': // DECSTBM - Set top and bottom margins decstbm(); break; case 's': // Save cursor (ANSI.SYS) if (type == DeviceType.XTERM) { savedState.cursorX = currentState.cursorX; savedState.cursorY = currentState.cursorY; } break; case 't': if (type == DeviceType.XTERM) { // Window operations xtermWindowOps(); } break; case 'u': // Restore cursor (ANSI.SYS) if (type == DeviceType.XTERM) { cursorPosition(savedState.cursorY, savedState.cursorX); } break; case 'v': case 'w': break; case 'x': // DECREQTPARM - Request terminal parameters decreqtparm(); break; case 'y': case 'z': case '{': case '|': case '}': case '~': break; } toGround(); } // 7F --> ignore // 0x9C goes to GROUND if (ch == 0x9C) { toGround(); } // 0x3A goes to CSI_IGNORE if (ch == 0x3A) { scanState = ScanState.CSI_IGNORE; } return; case CSI_PARAM: // 00-17, 19, 1C-1F --> execute if (ch <= 0x1F) { handleControlChar((char) ch); } // 20-2F --> collect, then switch to CSI_INTERMEDIATE if ((ch >= 0x20) && (ch <= 0x2F)) { collect((char) ch); scanState = ScanState.CSI_INTERMEDIATE; } // 30-39, 3B --> param if ((ch >= '0') && (ch <= '9')) { param((byte) ch); } if (ch == ';') { param((byte) ch); } // 0x3A goes to CSI_IGNORE if (ch == 0x3A) { scanState = ScanState.CSI_IGNORE; } // 0x3C-3F goes to CSI_IGNORE if ((ch >= 0x3C) && (ch <= 0x3F)) { scanState = ScanState.CSI_IGNORE; } // 40-7E --> dispatch, then switch to GROUND if ((ch >= 0x40) && (ch <= 0x7E)) { switch (ch) { case '@': // ICH - Insert character ich(); break; case 'A': // CUU - Cursor up cuu(); break; case 'B': // CUD - Cursor down cud(); break; case 'C': // CUF - Cursor forward cuf(); break; case 'D': // CUB - Cursor backward cub(); break; case 'E': // CNL - Cursor down and to column 1 if (type == DeviceType.XTERM) { cnl(); } break; case 'F': // CPL - Cursor up and to column 1 if (type == DeviceType.XTERM) { cpl(); } break; case 'G': // CHA - Cursor to column # in current row if (type == DeviceType.XTERM) { cha(); } break; case 'H': // CUP - Cursor position cup(); break; case 'I': // CHT - Cursor forward X tab stops (default 1) if (type == DeviceType.XTERM) { cht(); } break; case 'J': // ED - Erase in display ed(); break; case 'K': // EL - Erase in line el(); break; case 'L': // IL - Insert line il(); break; case 'M': // DL - Delete line dl(); break; case 'N': case 'O': break; case 'P': // DCH - Delete character dch(); break; case 'Q': case 'R': break; case 'S': // Scroll up X lines (default 1) if (type == DeviceType.XTERM) { 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': // Scroll down X lines (default 1) if (type == DeviceType.XTERM) { sd(); } break; case 'U': case 'V': case 'W': break; case 'X': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // ECH - Erase character ech(); } break; case 'Y': break; case 'Z': // CBT - Cursor backward X tab stops (default 1) if (type == DeviceType.XTERM) { cbt(); } break; case '[': case '\\': case ']': case '^': case '_': break; case '`': // HPA - Cursor to column # in current row. Same as CHA if (type == DeviceType.XTERM) { cha(); } break; case 'a': // HPR - Cursor right. Same as CUF if (type == DeviceType.XTERM) { cuf(); } break; case 'b': // REP - Repeat last char X times if (type == DeviceType.XTERM) { rep(); } break; case 'c': // DA - Device attributes da(); break; case 'd': // VPA - Cursor to row, current column. if (type == DeviceType.XTERM) { vpa(); } break; case 'e': // VPR - Cursor down. Same as CUD if (type == DeviceType.XTERM) { cud(); } break; case 'f': // HVP - Horizontal and vertical position hvp(); break; case 'g': // TBC - Tabulation clear tbc(); break; case 'h': // Sets an ANSI or DEC private toggle setToggle(true); break; case 'i': if ((type == DeviceType.VT220) || (type == DeviceType.XTERM)) { // Printer functions printerFunctions(); } break; case 'j': case 'k': break; case 'l': // Sets an ANSI or DEC private toggle setToggle(false); break; case 'm': // SGR - Select graphics rendition sgr(); break; case 'n': // DSR - Device status report dsr(); break; case 'o': case 'p': break; case 'q': // DECLL - Load leds // Not supported break; case 'r': // DECSTBM - Set top and bottom margins decstbm(); break; case 's': break; case 't': if (type == DeviceType.XTERM) { // Window operations xtermWindowOps(); } break; case 'u': case 'v': case 'w': break; case 'x': // DECREQTPARM - Request terminal parameters decreqtparm(); break; case 'y': case 'z': case '{': case '|': case '}': case '~': break; } toGround(); } // 7F --> ignore return; case CSI_INTERMEDIATE: // 00-17, 19, 1C-1F --> execute if (ch <= 0x1F) { handleControlChar((char) ch); } // 20-2F --> collect if ((ch >= 0x20) && (ch <= 0x2F)) { collect((char) ch); } // 0x30-3F goes to CSI_IGNORE if ((ch >= 0x30) && (ch <= 0x3F)) { scanState = ScanState.CSI_IGNORE; } // 40-7E --> dispatch, then switch to GROUND if ((ch >= 0x40) && (ch <= 0x7E)) { switch (ch) { case '@': case 'A': case 'B': case 'C': case 'D': case 'E': case 'F': case 'G': case 'H': case 'I': case 'J': case 'K': case 'L': case 'M': case 'N': case 'O': case 'P': case 'Q': case 'R': case 'S': case 'T': case 'U': case 'V': case 'W': case 'X': case 'Y': case 'Z': case '[': case '\\': case ']': case '^': case '_': case '`': case 'a': case 'b': case 'c': case 'd': case 'e': case 'f': case 'g': case 'h': case 'i': case 'j': case 'k': case 'l': case 'm': case 'n': case 'o': break; case 'p': if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (collectBuffer.charAt(collectBuffer.length() - 1) == '\"') ) { // DECSCL - compatibility level decscl(); } if ((type == DeviceType.XTERM) && (collectBuffer.charAt(collectBuffer.length() - 1) == '!') ) { // DECSTR - Soft terminal reset decstr(); } break; case 'q': if (((type == DeviceType.VT220) || (type == DeviceType.XTERM)) && (collectBuffer.charAt(collectBuffer.length() - 1) == '\"') ) { // DECSCA decsca(); } break; case 'r': case 's': case 't': case 'u': case 'v': case 'w': case 'x': case 'y': case 'z': case '{': case '|': case '}': case '~': break; } toGround(); } // 7F --> ignore return; case CSI_IGNORE: // 00-17, 19, 1C-1F --> execute if (ch <= 0x1F) { handleControlChar((char) ch); } // 20-2F --> collect if ((ch >= 0x20) && (ch <= 0x2F)) { collect((char) ch); } // 40-7E --> ignore, then switch to GROUND if ((ch >= 0x40) && (ch <= 0x7E)) { toGround(); } // 20-3F, 7F --> ignore return; case DCS_ENTRY: // 0x9C goes to GROUND if (ch == 0x9C) { toGround(); } // 0x1B 0x5C goes to GROUND if (ch == 0x1B) { collect((char) ch); } if (ch == 0x5C) { if ((collectBuffer.length() > 0) && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B) ) { toGround(); } } // 20-2F --> collect, then switch to DCS_INTERMEDIATE if ((ch >= 0x20) && (ch <= 0x2F)) { collect((char) ch); scanState = ScanState.DCS_INTERMEDIATE; } // 30-39, 3B --> param, then switch to DCS_PARAM if ((ch >= '0') && (ch <= '9')) { param((byte) ch); scanState = ScanState.DCS_PARAM; } if (ch == ';') { param((byte) ch); scanState = ScanState.DCS_PARAM; } // 3C-3F --> collect, then switch to DCS_PARAM if ((ch >= 0x3C) && (ch <= 0x3F)) { collect((char) ch); scanState = ScanState.DCS_PARAM; } // 00-17, 19, 1C-1F, 7F --> ignore // 0x3A goes to DCS_IGNORE if (ch == 0x3F) { scanState = ScanState.DCS_IGNORE; } // 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; case DCS_INTERMEDIATE: // 0x9C goes to GROUND if (ch == 0x9C) { toGround(); } // 0x1B 0x5C goes to GROUND if (ch == 0x1B) { collect((char) ch); } if (ch == 0x5C) { if ((collectBuffer.length() > 0) && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B) ) { toGround(); } } // 0x30-3F goes to DCS_IGNORE if ((ch >= 0x30) && (ch <= 0x3F)) { scanState = ScanState.DCS_IGNORE; } // 0x40-7E goes to DCS_PASSTHROUGH if ((ch >= 0x40) && (ch <= 0x7E)) { scanState = ScanState.DCS_PASSTHROUGH; } // 00-17, 19, 1C-1F, 7F --> ignore return; case DCS_PARAM: // 0x9C goes to GROUND if (ch == 0x9C) { toGround(); } // 0x1B 0x5C goes to GROUND if (ch == 0x1B) { collect((char) ch); } if (ch == 0x5C) { if ((collectBuffer.length() > 0) && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B) ) { toGround(); } } // 20-2F --> collect, then switch to DCS_INTERMEDIATE if ((ch >= 0x20) && (ch <= 0x2F)) { collect((char) ch); scanState = ScanState.DCS_INTERMEDIATE; } // 30-39, 3B --> param if ((ch >= '0') && (ch <= '9')) { param((byte) ch); } if (ch == ';') { param((byte) ch); } // 00-17, 19, 1C-1F, 7F --> ignore // 0x3A, 3C-3F goes to DCS_IGNORE if (ch == 0x3F) { scanState = ScanState.DCS_IGNORE; } if ((ch >= 0x3C) && (ch <= 0x3F)) { scanState = ScanState.DCS_IGNORE; } // 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; case DCS_PASSTHROUGH: // 0x9C goes to GROUND if (ch == 0x9C) { toGround(); } // 0x1B 0x5C goes to GROUND if (ch == 0x1B) { collect((char) ch); } if (ch == 0x5C) { if ((collectBuffer.length() > 0) && (collectBuffer.charAt(collectBuffer.length() - 1) == 0x1B) ) { toGround(); } } // 00-17, 19, 1C-1F, 20-7E --> put 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; } // 7F --> ignore return; case DCS_IGNORE: // 00-17, 19, 1C-1F, 20-7F --> ignore // 0x9C goes to GROUND if (ch == 0x9C) { toGround(); } 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(); } return; case OSC_STRING: // Special case for Xterm: OSC can pass control characters if ((ch == 0x9C) || (ch == 0x07) || (ch == 0x1B)) { oscPut((char) ch); } // 00-17, 19, 1C-1F --> ignore // 20-7F --> osc_put if ((ch >= 0x20) && (ch <= 0x7F)) { oscPut((char) ch); } // 0x9C goes to GROUND if (ch == 0x9C) { toGround(); } return; case VT52_DIRECT_CURSOR_ADDRESS: // This is a special case for the VT52 sequence "ESC Y l c" if (collectBuffer.length() == 0) { collect((char) ch); } else if (collectBuffer.length() == 1) { // We've got the two characters, one in the buffer and the // other in ch. cursorPosition(collectBuffer.charAt(0) - '\040', ch - '\040'); toGround(); } return; } } /** * Expose current cursor X to outside world. * * @return current cursor X */ public final int getCursorX() { if (display.get(currentState.cursorY).isDoubleWidth()) { return currentState.cursorX * 2; } return currentState.cursorX; } /** * Expose current cursor Y to outside world. * * @return current cursor Y */ public final int getCursorY() { 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; } byte [] bytes = StringUtils.fromBase64(data.getBytes()); 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 { byte [] bytes = StringUtils.fromBase64(data.getBytes()); 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(); if ((width != textWidth) || (height != textHeight)) { BufferedImage newImage; newImage = new BufferedImage(textWidth, textHeight, BufferedImage.TYPE_INT_ARGB); Graphics gr = newImage.getGraphics(); gr.drawImage(image.getSubimage(x * textWidth, y * textHeight, width, height), 0, 0, null, null); gr.dispose(); cell.setImage(newImage); } else { 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); // A real sixel terminal would render the text of the current // cell first, then image over it (accounting for blank // pixels). We do not support that. A cell is either text, // or image, but not a mix of image-over-text. 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); } } }