All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jexer.TTerminalWidget Maven / Gradle / Ivy

There is a newer version: 1.6.0
Show newest version
/*
 * 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 [[email protected]]
 * @version 1
 */
package jexer;

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;

import java.io.InputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;

import jexer.backend.ECMA48Terminal;
import jexer.backend.GlyphMaker;
import jexer.backend.MultiScreen;
import jexer.backend.SwingTerminal;
import jexer.bits.Cell;
import jexer.bits.CellAttributes;
import jexer.event.TKeypressEvent;
import jexer.event.TMenuEvent;
import jexer.event.TMouseEvent;
import jexer.event.TResizeEvent;
import jexer.menu.TMenu;
import jexer.tterminal.DisplayLine;
import jexer.tterminal.DisplayListener;
import jexer.tterminal.ECMA48;
import static jexer.TKeypress.*;

/**
 * TTerminalWidget exposes a ECMA-48 / ANSI X3.64 style terminal in a widget.
 */
public class TTerminalWidget extends TScrollableWidget
                             implements DisplayListener {

    /**
     * Translated strings.
     */
    private static final ResourceBundle i18n = ResourceBundle.getBundle(TTerminalWidget.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;

    /**
     * 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 visible display.
     */
    private List 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;

    /**
     * The return value from the emulator.
     */
    private int exitValue = -1;

    /**
     * Title to expose to a window.
     */
    private String title = "";

    /**
     * Action to perform when the terminal exits.
     */
    private TAction closeAction = null;

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Public constructor spawns a custom command line.
     *
     * @param parent parent widget
     * @param x column relative to parent
     * @param y row relative to parent
     * @param commandLine the command line to execute
     */
    public TTerminalWidget(final TWidget parent, final int x, final int y,
        final String commandLine) {

        this(parent, x, y, commandLine.split("\\s+"));
    }

    /**
     * Public constructor spawns a custom command line.
     *
     * @param parent parent widget
     * @param x column relative to parent
     * @param y row relative to parent
     * @param command the command line to execute
     */
    public TTerminalWidget(final TWidget parent, final int x, final int y,
        final String [] command) {

        this(parent, x, y, command, null);
    }

    /**
     * Public constructor spawns a custom command line.
     *
     * @param parent parent widget
     * @param x column relative to parent
     * @param y row relative to parent
     * @param command the command line to execute
     * @param closeAction action to perform when the shell sxits
     */
    public TTerminalWidget(final TWidget parent, final int x, final int y,
        final String [] command, final TAction closeAction) {

        this(parent, x, y, 80, 24, command, closeAction);
    }

    /**
     * Public constructor spawns a custom command line.
     *
     * @param parent parent widget
     * @param x column relative to parent
     * @param y row relative to parent
     * @param width width of widget
     * @param height height of widget
     * @param command the command line to execute
     * @param closeAction action to perform when the shell sxits
     */
    public TTerminalWidget(final TWidget parent, final int x, final int y,
        final int width, final int height, final String [] command,
        final TAction closeAction) {

        super(parent, x, y, width, height);

        this.closeAction = closeAction;

        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 parent parent widget
     * @param x column relative to parent
     * @param y row relative to parent
     */
    public TTerminalWidget(final TWidget parent, final int x, final int y) {
        this(parent, x, y, (TAction) null);
    }

    /**
     * Public constructor spawns a shell.
     *
     * @param parent parent widget
     * @param x column relative to parent
     * @param y row relative to parent
     * @param closeAction action to perform when the shell sxits
     */
    public TTerminalWidget(final TWidget parent, final int x, final int y,
        final TAction closeAction) {

        this(parent, x, y, 80, 24, closeAction);
    }

    /**
     * Public constructor spawns a shell.
     *
     * @param parent parent widget
     * @param x column relative to parent
     * @param y row relative to parent
     * @param width width of widget
     * @param height height of widget
     * @param closeAction action to perform when the shell sxits
     */
    public TTerminalWidget(final TWidget parent, final int x, final int y,
        final int width, final int height, final TAction closeAction) {

        super(parent, x, y, width, height);

        this.closeAction = closeAction;

        if (System.getProperty("jexer.TTerminal.shell") != null) {
            String shell = System.getProperty("jexer.TTerminal.shell");
            if (shell.trim().startsWith("ptypipe")) {
                ptypipe = true;
            }
            spawnShell(shell.split("\\s+"));
            return;
        }

        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+"));
        }
    }

    // ------------------------------------------------------------------------
    // Event handlers ---------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Handle window/screen resize events.
     *
     * @param resize resize event
     */
    @Override
    public void onResize(final TResizeEvent resize) {
        // Let TWidget set my size.
        super.onResize(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());
                    emulator.setHeight(getHeight());

                    emulator.writeRemote("\033[8;" + getHeight() + ";" +
                        getWidth() + "t");
                }

                // Pass the correct text cell width/height to the emulator
                if (getScreen() != null) {
                    emulator.setTextWidth(getScreen().getTextWidth());
                    emulator.setTextHeight(getScreen().getTextHeight());
                }
            }
            return;

        } // 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();
            dirty = true;
            return;
        }
        if (keypress.equals(kbShiftPgDn)
            || keypress.equals(kbCtrlPgDn)
            || keypress.equals(kbAltPgDn)
        ) {
            bigVerticalIncrement();
            dirty = true;
            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 (keypress.equals(kbEnter)) {
                if (System.getProperty("os.name").startsWith("Windows")
                    && (System.getProperty("jexer.TTerminal.cmdHack",
                            "true").equals("true"))
                ) {
                    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 (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();
                dirty = true;
                return;
            }
            if (mouse.isMouseWheelDown()) {
                verticalIncrement();
                dirty = true;
                return;
            }
        }
        if (mouseOnEmulator(mouse)) {
            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 (hideMouseWhenTyping) {
            typingHidMouse = false;
        }

        if (mouseOnEmulator(mouse)) {
            emulator.addUserEvent(mouse);
            readEmulatorState();
            return;
        }

        // Emulator didn't consume it, pass it on
        super.onMouseUp(mouse);
    }

    /**
     * Handle mouse motion events.
     *
     * @param mouse mouse motion event
     */
    @Override
    public void onMouseMotion(final TMouseEvent mouse) {
        if (hideMouseWhenTyping) {
            typingHidMouse = false;
        }

        if (mouseOnEmulator(mouse)) {
            emulator.addUserEvent(mouse);
            readEmulatorState();
            return;
        }

        // Emulator didn't consume it, pass it on
        super.onMouseMotion(mouse);
    }

    // ------------------------------------------------------------------------
    // TScrollableWidget ------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Draw the display buffer.
     */
    @Override
    public void draw() {
        int width = getDisplayWidth();

        boolean syncEmulator = false;
        if ((System.currentTimeMillis() - lastUpdateTime >= 20)
            && (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)
            || (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 (!isDrawable()) {
                    // We lost the connection, onShellExit() called an action
                    // that ultimately removed this widget from the UI
                    // hierarchy, so no one cares if we update the display.
                    // Bail out.
                    return;
                }

                if ((display == null) || emulator.isReading()) {
                    display = emulator.getVisibleDisplay(getHeight(),
                        -getVerticalValue());
                    assert (display.size() == getHeight());
                }
                width = emulator.getWidth();
            }
            dirty = false;
        }

        // Now draw the emulator screen
        int row = 0;
        for (DisplayLine line: display) {
            int widthMax = width;
            if (line.isDoubleWidth()) {
                widthMax /= 2;
            }
            if (widthMax > getWidth()) {
                widthMax = getWidth();
            }
            for (int i = 0; i < widthMax; i++) {
                Cell ch = line.charAt(i);

                if (ch.isImage()) {
                    putCharXY(i, 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), row, newCell);
                } else {
                    putCharXY(i, row, newCell);
                }
            }
            row++;
        }
    }

    /**
     * Set current value of the vertical scroll.
     *
     * @param value the new scroll value
     */
    @Override
    public void setVerticalValue(final int value) {
        super.setVerticalValue(value);
        dirty = true;
    }

    /**
     * Perform a small step change up.
     */
    @Override
    public void verticalDecrement() {
        super.verticalDecrement();
        dirty = true;
    }

    /**
     * Perform a small step change down.
     */
    @Override
    public void verticalIncrement() {
        super.verticalIncrement();
        dirty = true;
    }

    /**
     * Perform a big step change up.
     */
    public void bigVerticalDecrement() {
        super.bigVerticalDecrement();
        dirty = true;
    }

    /**
     * Perform a big step change down.
     */
    public void bigVerticalIncrement() {
        super.bigVerticalIncrement();
        dirty = true;
    }

    /**
     * Go to the top edge of the vertical scroller.
     */
    public void toTop() {
        super.toTop();
        dirty = true;
    }

    /**
     * Go to the bottom edge of the vertical scroller.
     */
    public void toBottom() {
        super.toBottom();
        dirty = true;
    }

    /**
     * Handle widget close.
     */
    @Override
    public void close() {
        emulator.close();
        if (shell != null) {
            terminateShellChildProcess();
            shell.destroy();
            shell = null;
        }
    }

    /**
     * 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()
                - (emulator.getScrollbackBuffer().size()
                    + emulator.getDisplayBuffer().size()));
            setVerticalBigChange(getHeight());

        } // synchronized (emulator)
    }

    // ------------------------------------------------------------------------
    // TTerminalWidget --------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Get the desired window title.
     *
     * @return the title
     */
    public String getTitle() {
        return title;
    }

    /**
     * Returns true if this widget does not want the application-wide mouse
     * cursor drawn over it.
     *
     * @return true if this widget does not want the application-wide mouse
     * cursor drawn over it
     */
    public boolean hasHiddenMouse() {
        return (emulator.hasHiddenMousePointer() || typingHidMouse);
    }

    /**
     * See if the terminal is still running.
     *
     * @return if true, we are still connected to / reading from the remote
     * side
     */
    public boolean isReading() {
        return emulator.isReading();
    }

    /**
     * Convert a string array to a whitespace-separated string.
     *
     * @param array the string array
     * @return a single string
     */
    private String stringArrayToString(final String [] array) {
        StringBuilder sb = new StringBuilder(array[0].length());
        for (int i = 0; i < array.length; i++) {
            sb.append(array[i]);
            if (i < array.length - 1) {
                sb.append(' ');
            }
        }
        return sb.toString();
    }

    /**
     * Spawn the shell.
     *
     * @param command the command line to execute
     */
    private void spawnShell(final String [] command) {

        /*
        System.err.printf("spawnShell(): '%s'\n",
            stringArrayToString(command));
        */

        // We will have vScroller for its data fields and mouse event
        // handling, but do not want to draw it.
        vScroller = new TVScroller(null, getWidth(), 0, getHeight());
        vScroller.setVisible(false);
        setBottomValue(0);

        title = i18n.getString("windowTitle");

        // Assume XTERM
        ECMA48.DeviceType deviceType = ECMA48.DeviceType.XTERM;

        try {
            ProcessBuilder pb = new ProcessBuilder(command);
            Map env = pb.environment();
            env.put("TERM", ECMA48.deviceTypeTerm(deviceType));
            env.put("LANG", ECMA48.deviceTypeLang(deviceType, "en"));
            env.put("COLUMNS", "80");
            env.put("LINES", "24");
            pb.redirectErrorStream(true);
            shell = pb.start();
            emulator = new ECMA48(deviceType, shell.getInputStream(),
                shell.getOutputStream(), this);
        } catch (IOException e) {
            messageBox(i18n.getString("errorLaunchingShellTitle"),
                MessageFormat.format(i18n.getString("errorLaunchingShellText"),
                    e.getMessage()));
        }

        // Setup the scroll bars
        onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(),
                getHeight()));

        // Hide mouse when typing option
        if (System.getProperty("jexer.TTerminal.hideMouseWhenTyping",
                "true").equals("false")) {

            hideMouseWhenTyping = false;
        }
    }

    /**
     * Terminate the child of the 'script' process used on POSIX.  This may
     * or may not work.
     */
    private void terminateShellChildProcess() {
        int pid = -1;
        if (shell.getClass().getName().equals("java.lang.UNIXProcess")) {
            /* get the PID on unix/linux systems */
            try {
                Field field = shell.getClass().getDeclaredField("pid");
                field.setAccessible(true);
                pid = field.getInt(shell);
            } catch (Throwable e) {
                // SQUASH, this didn't work.  Just bail out quietly.
                return;
            }
        }
        if (pid != -1) {
            // shell.destroy() works successfully at killing this side of
            // 'script'.  But we need to make sure the other side (child
            // process) is also killed.
            String [] cmdKillIt = {
                "pkill", "-P", Integer.toString(pid)
            };
            try {
                Runtime.getRuntime().exec(cmdKillIt);
            } catch (Throwable e) {
                // SQUASH, this didn't work.  Just bail out quietly.
                return;
            }
        }
    }

    /**
     * Hook for subclasses to be notified of the shell termination.
     */
    public void onShellExit() {
        TApplication app = getApplication();
        if (app != null) {
            if (closeAction != null) {
                // We have to put this action inside invokeLater() because it
                // could be executed during draw() when syncing with ECMA48.
                app.invokeLater(new Runnable() {
                    public void run() {
                        closeAction.DO(TTerminalWidget.this);
                    }
                });
            }
            if (getApplication() != null) {
                getApplication().postEvent(new TMenuEvent(
                    TMenu.MID_REPAINT));
            }
        }
    }

    /**
     * Copy out variables from the emulator that TTerminal has to expose on
     * screen.
     */
    private void readEmulatorState() {
        // Synchronize against the emulator so we don't stomp on its reader
        // thread.
        synchronized (emulator) {

            setCursorX(emulator.getCursorX());
            setCursorY(emulator.getCursorY()
                + (getHeight() - emulator.getHeight())
                - getVerticalValue());
            setCursorVisible(emulator.isCursorVisible());
            if (getCursorX() > getWidth()) {
                setCursorVisible(false);
            }
            if ((getCursorY() >= getHeight()) || (getCursorY() < 0)) {
                setCursorVisible(false);
            }
            if (emulator.getScreenTitle().length() > 0) {
                // Only update the title if the shell is still alive
                if (shell != null) {
                    title = emulator.getScreenTitle();
                }
            }

            // Check to see if the shell has died.
            if (!emulator.isReading() && (shell != null)) {
                try {
                    int rc = shell.exitValue();
                    // The emulator exited on its own, all is fine
                    title = MessageFormat.format(i18n.
                        getString("windowTitleCompleted"), title, rc);
                    exitValue = rc;
                    shell = null;
                    emulator.close();
                    onShellExit();
                } catch (IllegalThreadStateException e) {
                    // The emulator thread has exited, but the shell Process
                    // hasn't figured that out yet.  Do nothing, we will see
                    // this in a future tick.
                }
            } else if (emulator.isReading() && (shell != null)) {
                // The shell might be dead, let's check
                try {
                    int rc = shell.exitValue();
                    // If we got here, the shell died.
                    title = MessageFormat.format(i18n.
                        getString("windowTitleCompleted"), title, rc);
                    exitValue = rc;
                    shell = null;
                    emulator.close();
                    onShellExit();
                } catch (IllegalThreadStateException e) {
                    // The shell is still running, do nothing.
                }
            }

        } // synchronized (emulator)
    }

    /**
     * Check if a mouse press/release/motion event coordinate is over the
     * emulator.
     *
     * @param mouse a mouse-based event
     * @return whether or not the mouse is on the emulator
     */
    private boolean mouseOnEmulator(final TMouseEvent mouse) {

        if (!emulator.isReading()) {
            return false;
        }

        if ((mouse.getX() >= 0)
            && (mouse.getX() < getWidth() - 1)
            && (mouse.getY() >= 0)
            && (mouse.getY() < getHeight())
        ) {
            return true;
        }
        return false;
    }

    /**
     * Draw glyphs for a double-width or double-height VT100 cell to two
     * screen cells.
     *
     * @param line the line this VT100 cell is in
     * @param x the X position to draw the left half to
     * @param y the Y position to draw to
     * @param cell the cell to draw
     */
    private void putDoubleWidthCharXY(final DisplayLine line, final int x,
        final int y, final Cell cell) {

        int textWidth = getScreen().getTextWidth();
        int textHeight = getScreen().getTextHeight();
        boolean cursorBlinkVisible = true;

        if (getScreen() instanceof SwingTerminal) {
            SwingTerminal terminal = (SwingTerminal) getScreen();
            cursorBlinkVisible = terminal.getCursorBlinkVisible();
        } else if (getScreen() instanceof ECMA48Terminal) {
            ECMA48Terminal terminal = (ECMA48Terminal) getScreen();

            if (!terminal.hasSixel()) {
                // The backend does not have sixel support, draw this as text
                // and bail out.
                putCharXY(x, y, cell);
                putCharXY(x + 1, y, ' ', cell);
                return;
            }
            cursorBlinkVisible = blinkState;
        } else {
            // We don't know how to dray glyphs to this screen, draw them as
            // text and bail out.
            putCharXY(x, y, cell);
            putCharXY(x + 1, y, ' ', cell);
            return;
        }

        if ((textWidth != lastTextWidth) || (textHeight != lastTextHeight)) {
            // Screen size has changed, reset the font.
            setupFont(textHeight);
            lastTextWidth = textWidth;
            lastTextHeight = textHeight;
        }
        assert (doubleFont != null);

        BufferedImage image;
        if (line.getDoubleHeight() == 1) {
            // Double-height top half: don't draw the underline.
            Cell newCell = new Cell(cell);
            newCell.setUnderline(false);
            image = doubleFont.getImage(newCell, textWidth * 2, textHeight * 2,
                cursorBlinkVisible);
        } else {
            image = doubleFont.getImage(cell,  textWidth * 2, textHeight * 2,
                cursorBlinkVisible);
        }

        // Now that we have the double-wide glyph drawn, copy the right
        // pieces of it to the cells.
        Cell left = new Cell(cell);
        Cell right = new Cell(cell);
        right.setChar(' ');
        BufferedImage leftImage = null;
        BufferedImage rightImage = null;
        /*
        System.err.println("image " + image + " textWidth " + textWidth +
            " textHeight " + textHeight);
         */

        switch (line.getDoubleHeight()) {
        case 1:
            // Top half double height
            leftImage = image.getSubimage(0, 0, textWidth, textHeight);
            rightImage = image.getSubimage(textWidth, 0, textWidth, textHeight);
            break;
        case 2:
            // Bottom half double height
            leftImage = image.getSubimage(0, textHeight, textWidth, textHeight);
            rightImage = image.getSubimage(textWidth, textHeight,
                textWidth, textHeight);
            break;
        default:
            // Either single height double-width, or error fallback
            BufferedImage wideImage = new BufferedImage(textWidth * 2,
                textHeight, BufferedImage.TYPE_INT_ARGB);
            Graphics2D grWide = wideImage.createGraphics();
            grWide.drawImage(image, 0, 0, wideImage.getWidth(),
                wideImage.getHeight(), null);
            grWide.dispose();
            leftImage = wideImage.getSubimage(0, 0, textWidth, textHeight);
            rightImage = wideImage.getSubimage(textWidth, 0, textWidth,
                textHeight);
            break;
        }
        left.setImage(leftImage);
        right.setImage(rightImage);
        // Since we have image data, ditch the character here.  Otherwise, a
        // drawBoxShadow() over the terminal window will show the characters
        // which looks wrong.
        left.setChar(' ');
        right.setChar(' ');
        putCharXY(x, y, left);
        putCharXY(x + 1, y, right);
    }

    /**
     * Set up the double-width font.
     *
     * @param fontSize the size of font to request for the single-width font.
     * The double-width font will be 2x this value.
     */
    private void setupFont(final int fontSize) {
        doubleFont = GlyphMaker.getInstance(fontSize * 2);

        // Special case: the ECMA48 backend needs to have a timer to drive
        // its blink state.
        if (getScreen() instanceof jexer.backend.ECMA48Terminal) {
            if (!haveTimer) {
                // Blink every 500 millis.
                long millis = 500;
                getApplication().addTimer(millis, true,
                    new TAction() {
                        public void DO() {
                            blinkState = !blinkState;
                            getApplication().doRepaint();
                        }
                    }
                );
                haveTimer = true;
            }
        }
    }

    // ------------------------------------------------------------------------
    // DisplayListener --------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Called by emulator when fresh data has come in.
     */
    public void displayChanged() {
        dirty = true;
        getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
    }

    /**
     * Function to call to obtain the display width.
     *
     * @return the number of columns in the display
     */
    public int getDisplayWidth() {
        if (ptypipe) {
            return getWidth();
        }
        return 80;
    }

    /**
     * Function to call to obtain the display height.
     *
     * @return the number of rows in the display
     */
    public int getDisplayHeight() {
        if (ptypipe) {
            return getHeight();
        }
        return 24;
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy