+public class TTerminalWindow extends TScrollableWindow
+ implements DisplayListener {
+
+ /**
+ * Translated strings.
+ */
+ private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWindow.class.getName());
+
+ // ------------------------------------------------------------------------
+ // Variables --------------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * The emulator.
+ */
+ private ECMA48 emulator;
+
+ /**
+ * The Process created by the shell spawning constructor.
+ */
+ private Process shell;
+
+ /**
+ * If true, we are using the ptypipe utility to support dynamic window
+ * resizing. ptypipe is available at
+ * https://gitlab.com/klamonte/ptypipe .
+ */
+ private boolean ptypipe = false;
+
+ /**
+ * If true, close the window when the shell exits.
+ */
+ private boolean closeOnExit = false;
+
+ /**
+ * Double-height font.
+ */
+ private GlyphMaker doubleFont;
+
+ /**
+ * Last text width value.
+ */
+ private int lastTextWidth = -1;
+
+ /**
+ * Last text height value.
+ */
+ private int lastTextHeight = -1;
+
+ /**
+ * The blink state, used only by ECMA48 backend and when double-width
+ * chars must be drawn.
+ */
+ private boolean blinkState = true;
+
+ /**
+ * Timer flag, used only by ECMA48 backend and when double-width chars
+ * must be drawn.
+ */
+ private boolean haveTimer = false;
+
+ /**
+ * The last seen scrollback lines.
+ */
+ private List<DisplayLine> scrollback;
+
+ /**
+ * The last seen display lines.
+ */
+ private List<DisplayLine> display;
+
+ /**
+ * If true, the display has changed and needs updating.
+ */
+ private volatile boolean dirty = true;
+
+ /**
+ * Time that the display was last updated.
+ */
+ private long lastUpdateTime = 0;
+
+ /**
+ * If true, hide the mouse after typing a keystroke.
+ */
+ private boolean hideMouseWhenTyping = true;
+
+ /**
+ * If true, the mouse should not be displayed because a keystroke was
+ * typed.
+ */
+ private boolean typingHidMouse = false;
+
+ // ------------------------------------------------------------------------
+ // Constructors -----------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param commandLine the command line to execute
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final String commandLine) {
+
+ this(application, x, y, RESIZABLE, commandLine.split("\\s+"),
+ System.getProperty("jexer.TTerminal.closeOnExit",
+ "false").equals("true"));
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param commandLine the command line to execute
+ * @param closeOnExit if true, close the window when the command exits
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final String commandLine, final boolean closeOnExit) {
+
+ this(application, x, y, RESIZABLE, commandLine.split("\\s+"),
+ closeOnExit);
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param command the command line to execute
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final int flags, final String [] command) {
+
+ this(application, x, y, flags, command,
+ System.getProperty("jexer.TTerminal.closeOnExit",
+ "false").equals("true"));
+ }
+
+ /**
+ * Public constructor spawns a custom command line.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param command the command line to execute
+ * @param closeOnExit if true, close the window when the command exits
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final int flags, final String [] command,
+ final boolean closeOnExit) {
+
+ super(application, i18n.getString("windowTitle"), x, y,
+ 80 + 2, 24 + 2, flags);
+
+ this.closeOnExit = closeOnExit;
+
+ String [] fullCommand;
+
+ // Spawn a shell and pass its I/O to the other constructor.
+ if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
+ && (System.getProperty("jexer.TTerminal.ptypipe").
+ equals("true"))
+ ) {
+ ptypipe = true;
+ fullCommand = new String[command.length + 1];
+ fullCommand[0] = "ptypipe";
+ System.arraycopy(command, 0, fullCommand, 1, command.length);
+ } else if (System.getProperty("os.name").startsWith("Windows")) {
+ fullCommand = new String[3];
+ fullCommand[0] = "cmd";
+ fullCommand[1] = "/c";
+ fullCommand[2] = stringArrayToString(command);
+ } else if (System.getProperty("os.name").startsWith("Mac")) {
+ fullCommand = new String[6];
+ fullCommand[0] = "script";
+ fullCommand[1] = "-q";
+ fullCommand[2] = "-F";
+ fullCommand[3] = "/dev/null";
+ fullCommand[4] = "-c";
+ fullCommand[5] = stringArrayToString(command);
+ } else {
+ // Default: behave like Linux
+ fullCommand = new String[5];
+ fullCommand[0] = "script";
+ fullCommand[1] = "-fqe";
+ fullCommand[2] = "/dev/null";
+ fullCommand[3] = "-c";
+ fullCommand[4] = stringArrayToString(command);
+ }
+ spawnShell(fullCommand);
+ }
+
+ /**
+ * Public constructor spawns a shell.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final int flags) {
+
+ this(application, x, y, flags,
+ System.getProperty("jexer.TTerminal.closeOnExit",
+ "false").equals("true"));
+
+ }
+
+ /**
+ * Public constructor spawns a shell.
+ *
+ * @param application TApplication that manages this window
+ * @param x column relative to parent
+ * @param y row relative to parent
+ * @param flags mask of CENTERED, MODAL, or RESIZABLE
+ * @param closeOnExit if true, close the window when the shell exits
+ */
+ public TTerminalWindow(final TApplication application, final int x,
+ final int y, final int flags, final boolean closeOnExit) {
+
+ super(application, i18n.getString("windowTitle"), x, y,
+ 80 + 2, 24 + 2, flags);
+
+ this.closeOnExit = closeOnExit;
+
+ String cmdShellWindows = "cmd.exe";
+
+ // You cannot run a login shell in a bare Process interactively, due
+ // to libc's behavior of buffering when stdin/stdout aren't a tty.
+ // Use 'script' instead to run a shell in a pty. And because BSD and
+ // GNU differ on the '-f' vs '-F' flags, we need two different
+ // commands. Lovely.
+ String cmdShellGNU = "script -fqe /dev/null";
+ String cmdShellBSD = "script -q -F /dev/null";
+
+ // ptypipe is another solution that permits dynamic window resizing.
+ String cmdShellPtypipe = "ptypipe /bin/bash --login";
+
+ // Spawn a shell and pass its I/O to the other constructor.
+ if ((System.getProperty("jexer.TTerminal.ptypipe") != null)
+ && (System.getProperty("jexer.TTerminal.ptypipe").
+ equals("true"))
+ ) {
+ ptypipe = true;
+ spawnShell(cmdShellPtypipe.split("\\s+"));
+ } else if (System.getProperty("os.name").startsWith("Windows")) {
+ spawnShell(cmdShellWindows.split("\\s+"));
+ } else if (System.getProperty("os.name").startsWith("Mac")) {
+ spawnShell(cmdShellBSD.split("\\s+"));
+ } else if (System.getProperty("os.name").startsWith("Linux")) {
+ spawnShell(cmdShellGNU.split("\\s+"));
+ } else {
+ // When all else fails, assume GNU.
+ spawnShell(cmdShellGNU.split("\\s+"));
+ }
+ }
+
+ // ------------------------------------------------------------------------
+ // TScrollableWindow ------------------------------------------------------
+ // ------------------------------------------------------------------------
+
+ /**
+ * Draw the display buffer.
+ */
+ @Override
+ public void draw() {
+ int width = getDisplayWidth();
+ boolean syncEmulator = false;
+ if ((System.currentTimeMillis() - lastUpdateTime >= 25)
+ && (dirty == true)
+ ) {
+ // Too much time has passed, draw it all.
+ syncEmulator = true;
+ } else if (emulator.isReading() && (dirty == false)) {
+ // Wait until the emulator has brought more data in.
+ syncEmulator = false;
+ } else if (!emulator.isReading() && (dirty == true)) {
+ // The emulator won't receive more data, update the display.
+ syncEmulator = true;
+ }
+
+ if ((syncEmulator == true)
+ || (scrollback == null)
+ || (display == null)
+ ) {
+ // We want to minimize the amount of time we have the emulator
+ // locked. Grab a copy of its display.
+ synchronized (emulator) {
+ // Update the scroll bars
+ reflowData();
+
+ if ((scrollback == null) || emulator.isReading()) {
+ scrollback = copyBuffer(emulator.getScrollbackBuffer());
+ display = copyBuffer(emulator.getDisplayBuffer());
+ }
+ width = emulator.getWidth();
+ }
+ dirty = false;
+ }
+
+ // Draw the box using my superclass
+ super.draw();
+
+ // Put together the visible rows
+ int visibleHeight = getHeight() - 2;
+ int visibleBottom = scrollback.size() + display.size()
+ + getVerticalValue();
+ assert (visibleBottom >= 0);
+
+ List<DisplayLine> preceedingBlankLines = new ArrayList<DisplayLine>();
+ int visibleTop = visibleBottom - visibleHeight;
+ if (visibleTop < 0) {
+ for (int i = visibleTop; i < 0; i++) {
+ preceedingBlankLines.add(emulator.getBlankDisplayLine());
+ }
+ visibleTop = 0;
+ }
+ assert (visibleTop >= 0);
+
+ List<DisplayLine> displayLines = new ArrayList<DisplayLine>();
+ displayLines.addAll(scrollback);
+ displayLines.addAll(display);
+
+ List<DisplayLine> visibleLines = new ArrayList<DisplayLine>();
+ visibleLines.addAll(preceedingBlankLines);
+ visibleLines.addAll(displayLines.subList(visibleTop,
+ visibleBottom));
+
+ visibleHeight -= visibleLines.size();
+ assert (visibleHeight >= 0);
+
+ // Now draw the emulator screen
+ int row = 1;
+ for (DisplayLine line: visibleLines) {
+ int widthMax = width;
+ if (line.isDoubleWidth()) {
+ widthMax /= 2;
+ }
+ if (widthMax > getWidth() - 2) {
+ widthMax = getWidth() - 2;
+ }
+ for (int i = 0; i < widthMax; i++) {
+ Cell ch = line.charAt(i);
+
+ if (ch.isImage()) {
+ putCharXY(i + 1, row, ch);
+ continue;
+ }
+
+ Cell newCell = new Cell(ch);
+ boolean reverse = line.isReverseColor() ^ ch.isReverse();
+ newCell.setReverse(false);
+ if (reverse) {
+ if (ch.getForeColorRGB() < 0) {
+ newCell.setBackColor(ch.getForeColor());
+ newCell.setBackColorRGB(-1);
+ } else {
+ newCell.setBackColorRGB(ch.getForeColorRGB());
+ }
+ if (ch.getBackColorRGB() < 0) {
+ newCell.setForeColor(ch.getBackColor());
+ newCell.setForeColorRGB(-1);
+ } else {
+ newCell.setForeColorRGB(ch.getBackColorRGB());
+ }
+ }
+ if (line.isDoubleWidth()) {
+ putDoubleWidthCharXY(line, (i * 2) + 1, row, newCell);
+ } else {
+ putCharXY(i + 1, row, newCell);
+ }
+ }
+ row++;
+ if (row == getHeight() - 1) {
+ // Don't overwrite the box edge
+ break;
+ }
+ }
+ CellAttributes background = new CellAttributes();
+ // Fill in the blank lines on bottom
+ for (int i = 0; i < visibleHeight; i++) {
+ hLineXY(1, i + row, getWidth() - 2, ' ', background);
+ }
+
+ }
+
+ /**
+ * Handle window close.
+ */
+ @Override
+ public void onClose() {
+ emulator.close();
+ if (shell != null) {
+ terminateShellChildProcess();
+ shell.destroy();
+ shell = null;
+ }
+ }
+
+ /**
+ * Handle window/screen resize events.
+ *
+ * @param resize resize event
+ */
+ @Override
+ public void onResize(final TResizeEvent resize) {
+
+ // Synchronize against the emulator so we don't stomp on its reader
+ // thread.
+ synchronized (emulator) {
+
+ if (resize.getType() == TResizeEvent.Type.WIDGET) {
+ // Resize the scroll bars
+ reflowData();
+ placeScrollbars();
+
+ // Get out of scrollback
+ setVerticalValue(0);
+
+ if (ptypipe) {
+ emulator.setWidth(getWidth() - 2);
+ emulator.setHeight(getHeight() - 2);
+
+ emulator.writeRemote("\033[8;" + (getHeight() - 2) + ";" +
+ (getWidth() - 2) + "t");
+ }
+ }
+ return;
+
+ } // synchronized (emulator)
+ }
+
+ /**
+ * Resize scrollbars for a new width/height.
+ */
+ @Override
+ public void reflowData() {
+
+ // Synchronize against the emulator so we don't stomp on its reader
+ // thread.
+ synchronized (emulator) {
+
+ // Pull cursor information
+ readEmulatorState();
+
+ // Vertical scrollbar
+ setTopValue(getHeight() - 2
+ - (emulator.getScrollbackBuffer().size()
+ + emulator.getDisplayBuffer().size()));
+ setVerticalBigChange(getHeight() - 2);
+
+ } // synchronized (emulator)
+ }
+
+ /**
+ * Handle keystrokes.
+ *
+ * @param keypress keystroke event
+ */
+ @Override
+ public void onKeypress(final TKeypressEvent keypress) {
+ if (hideMouseWhenTyping) {
+ typingHidMouse = true;
+ }
+
+ // Scrollback up/down
+ if (keypress.equals(kbShiftPgUp)
+ || keypress.equals(kbCtrlPgUp)
+ || keypress.equals(kbAltPgUp)
+ ) {
+ bigVerticalDecrement();
+ return;
+ }
+ if (keypress.equals(kbShiftPgDn)
+ || keypress.equals(kbCtrlPgDn)
+ || keypress.equals(kbAltPgDn)
+ ) {
+ bigVerticalIncrement();
+ return;
+ }
+
+ if (emulator.isReading()) {
+ // Get out of scrollback
+ setVerticalValue(0);
+ emulator.addUserEvent(keypress);
+
+ // UGLY HACK TIME! cmd.exe needs CRLF, not just CR, so if
+ // this is kBEnter then also send kbCtrlJ.
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ if (keypress.equals(kbEnter)) {
+ emulator.addUserEvent(new TKeypressEvent(kbCtrlJ));
+ }
+ }
+
+ readEmulatorState();
+ return;
+ }
+
+ // Process is closed, honor "normal" TUI keystrokes
+ super.onKeypress(keypress);
+ }
+
+ /**
+ * Handle mouse press events.
+ *
+ * @param mouse mouse button press event
+ */
+ @Override
+ public void onMouseDown(final TMouseEvent mouse) {
+ if (inWindowMove || inWindowResize) {
+ // TWindow needs to deal with this.
+ super.onMouseDown(mouse);
+ return;
+ }
+
+ if (hideMouseWhenTyping) {
+ typingHidMouse = false;
+ }
+
+ // If the emulator is tracking mouse buttons, it needs to see wheel
+ // events.
+ if (emulator.getMouseProtocol() == ECMA48.MouseProtocol.OFF) {
+ if (mouse.isMouseWheelUp()) {
+ verticalDecrement();
+ return;
+ }
+ if (mouse.isMouseWheelDown()) {
+ verticalIncrement();
+ return;
+ }
+ }
+ if (mouseOnEmulator(mouse)) {
+ mouse.setX(mouse.getX() - 1);
+ mouse.setY(mouse.getY() - 1);
+ emulator.addUserEvent(mouse);
+ readEmulatorState();
+ return;
+ }
+
+ // Emulator didn't consume it, pass it on
+ super.onMouseDown(mouse);
+ }
+
+ /**
+ * Handle mouse release events.
+ *
+ * @param mouse mouse button release event
+ */
+ @Override
+ public void onMouseUp(final TMouseEvent mouse) {
+ if (inWindowMove || inWindowResize) {
+ // TWindow needs to deal with this.
+ super.onMouseUp(mouse);
+ return;
+ }
+
+ if (hideMouseWhenTyping) {
+ typingHidMouse = false;
+ }
+
+ if (mouseOnEmulator(mouse)) {
+ mouse.setX(mouse.getX() - 1);
+ mouse.setY(mouse.getY() - 1);
+ emulator.addUserEvent(mouse);
+ readEmulatorState();
+ return;
+ }