Merge branch 'subtree'
[fanfix.git] / src / jexer / TTerminalWidget.java
index 7a93f4e7b32320c69ade2090049146e9200fdef0..bf51e6b5c2fd67b78b55d6e280e44f68d3b335aa 100644 (file)
  */
 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.File;
 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.TCommandEvent;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMenuEvent;
 import jexer.event.TMouseEvent;
@@ -57,13 +51,14 @@ import jexer.menu.TMenu;
 import jexer.tterminal.DisplayLine;
 import jexer.tterminal.DisplayListener;
 import jexer.tterminal.ECMA48;
+import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
  * TTerminalWidget exposes a ECMA-48 / ANSI X3.64 style terminal in a widget.
  */
 public class TTerminalWidget extends TScrollableWidget
-                             implements DisplayListener {
+                             implements DisplayListener, EditMenuUser {
 
     /**
      * Translated strings.
@@ -84,6 +79,11 @@ public class TTerminalWidget extends TScrollableWidget
      */
     private Process shell;
 
+    /**
+     * If true, something called 'ptypipe' is on the PATH and executable.
+     */
+    private static boolean ptypipeOnPath = false;
+
     /**
      * If true, we are using the ptypipe utility to support dynamic window
      * resizing.  ptypipe is available at
@@ -119,12 +119,7 @@ public class TTerminalWidget extends TScrollableWidget
     private boolean haveTimer = false;
 
     /**
-     * The last seen scrollback lines.
-     */
-    private List<DisplayLine> scrollback;
-
-    /**
-     * The last seen display lines.
+     * The last seen visible display.
      */
     private List<DisplayLine> display;
 
@@ -168,6 +163,13 @@ public class TTerminalWidget extends TScrollableWidget
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Static constructor.
+     */
+    static {
+        checkForPtypipe();
+    }
+
     /**
      * Public constructor spawns a custom command line.
      *
@@ -203,7 +205,7 @@ public class TTerminalWidget extends TScrollableWidget
      * @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
+     * @param closeAction action to perform when the shell exits
      */
     public TTerminalWidget(final TWidget parent, final int x, final int y,
         final String [] command, final TAction closeAction) {
@@ -220,7 +222,7 @@ public class TTerminalWidget extends TScrollableWidget
      * @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
+     * @param closeAction action to perform when the shell exits
      */
     public TTerminalWidget(final TWidget parent, final int x, final int y,
         final int width, final int height, final String [] command,
@@ -241,6 +243,14 @@ public class TTerminalWidget extends TScrollableWidget
             fullCommand = new String[command.length + 1];
             fullCommand[0] = "ptypipe";
             System.arraycopy(command, 0, fullCommand, 1, command.length);
+        } else if (System.getProperty("jexer.TTerminal.ptypipe",
+                "auto").equals("auto")
+            && (ptypipeOnPath == 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";
@@ -256,12 +266,24 @@ public class TTerminalWidget extends TScrollableWidget
             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);
+            if (System.getProperty("jexer.TTerminal.setsid",
+                    "true").equals("false")
+            ) {
+                fullCommand = new String[5];
+                fullCommand[0] = "script";
+                fullCommand[1] = "-fqe";
+                fullCommand[2] = "/dev/null";
+                fullCommand[3] = "-c";
+                fullCommand[4] = stringArrayToString(command);
+            } else {
+                fullCommand = new String[6];
+                fullCommand[0] = "setsid";
+                fullCommand[1] = "script";
+                fullCommand[2] = "-fqe";
+                fullCommand[3] = "/dev/null";
+                fullCommand[4] = "-c";
+                fullCommand[5] = stringArrayToString(command);
+            }
         }
         spawnShell(fullCommand);
     }
@@ -283,7 +305,7 @@ public class TTerminalWidget extends TScrollableWidget
      * @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
+     * @param closeAction action to perform when the shell exits
      */
     public TTerminalWidget(final TWidget parent, final int x, final int y,
         final TAction closeAction) {
@@ -299,7 +321,7 @@ public class TTerminalWidget extends TScrollableWidget
      * @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
+     * @param closeAction action to perform when the shell exits
      */
     public TTerminalWidget(final TWidget parent, final int x, final int y,
         final int width, final int height, final TAction closeAction) {
@@ -325,6 +347,7 @@ public class TTerminalWidget extends TScrollableWidget
         // GNU differ on the '-f' vs '-F' flags, we need two different
         // commands.  Lovely.
         String cmdShellGNU = "script -fqe /dev/null";
+        String cmdShellGNUSetsid = "setsid script -fqe /dev/null";
         String cmdShellBSD = "script -q -F /dev/null";
 
         // ptypipe is another solution that permits dynamic window resizing.
@@ -337,12 +360,24 @@ public class TTerminalWidget extends TScrollableWidget
         ) {
             ptypipe = true;
             spawnShell(cmdShellPtypipe.split("\\s+"));
+        } else if (System.getProperty("jexer.TTerminal.ptypipe",
+                "auto").equals("auto")
+            && (ptypipeOnPath == 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+"));
+            if (System.getProperty("jexer.TTerminal.setsid",
+                    "true").equals("false")
+            ) {
+                spawnShell(cmdShellGNU.split("\\s+"));
+            } else {
+                spawnShell(cmdShellGNUSetsid.split("\\s+"));
+            }
         } else {
             // When all else fails, assume GNU.
             spawnShell(cmdShellGNU.split("\\s+"));
@@ -350,145 +385,9 @@ public class TTerminalWidget extends TScrollableWidget
     }
 
     // ------------------------------------------------------------------------
-    // TScrollableWidget ------------------------------------------------------
+    // Event handlers ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
-    /**
-     * 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;
-        }
-
-        // Put together the visible rows
-        int visibleHeight = getHeight();
-        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 = 0;
-        for (DisplayLine line: visibleLines) {
-            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++;
-            if (row == getHeight()) {
-                // 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(0, i + row, getWidth(), ' ', background);
-        }
-
-    }
-
-    /**
-     * Handle widget close.
-     */
-    @Override
-    public void close() {
-        emulator.close();
-        if (shell != null) {
-            terminateShellChildProcess();
-            shell.destroy();
-            shell = null;
-        }
-    }
-
     /**
      * Handle window/screen resize events.
      *
@@ -499,6 +398,10 @@ public class TTerminalWidget extends TScrollableWidget
         // Let TWidget set my size.
         super.onResize(resize);
 
+        if (emulator == null) {
+            return;
+        }
+
         // Synchronize against the emulator so we don't stomp on its reader
         // thread.
         synchronized (emulator) {
@@ -530,28 +433,6 @@ public class TTerminalWidget extends TScrollableWidget
         } // 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()
-                - (emulator.getScrollbackBuffer().size()
-                    + emulator.getDisplayBuffer().size()));
-            setVerticalBigChange(getHeight());
-
-        } // synchronized (emulator)
-    }
-
     /**
      * Handle keystrokes.
      *
@@ -569,6 +450,7 @@ public class TTerminalWidget extends TScrollableWidget
             || keypress.equals(kbAltPgUp)
         ) {
             bigVerticalDecrement();
+            dirty = true;
             return;
         }
         if (keypress.equals(kbShiftPgDn)
@@ -576,10 +458,11 @@ public class TTerminalWidget extends TScrollableWidget
             || keypress.equals(kbAltPgDn)
         ) {
             bigVerticalIncrement();
+            dirty = true;
             return;
         }
 
-        if (emulator.isReading()) {
+        if ((emulator != null) && (emulator.isReading())) {
             // Get out of scrollback
             setVerticalValue(0);
             emulator.addUserEvent(keypress);
@@ -614,23 +497,27 @@ public class TTerminalWidget extends TScrollableWidget
             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 (emulator != null) {
+            // 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 (mouse.isMouseWheelDown()) {
-                verticalIncrement();
+            if (mouseOnEmulator(mouse)) {
+                emulator.addUserEvent(mouse);
+                readEmulatorState();
                 return;
             }
         }
-        if (mouseOnEmulator(mouse)) {
-            emulator.addUserEvent(mouse);
-            readEmulatorState();
-            return;
-        }
 
         // Emulator didn't consume it, pass it on
         super.onMouseDown(mouse);
@@ -647,7 +534,7 @@ public class TTerminalWidget extends TScrollableWidget
             typingHidMouse = false;
         }
 
-        if (mouseOnEmulator(mouse)) {
+        if ((emulator != null) && (mouseOnEmulator(mouse))) {
             emulator.addUserEvent(mouse);
             readEmulatorState();
             return;
@@ -668,7 +555,7 @@ public class TTerminalWidget extends TScrollableWidget
             typingHidMouse = false;
         }
 
-        if (mouseOnEmulator(mouse)) {
+        if ((emulator != null) && (mouseOnEmulator(mouse))) {
             emulator.addUserEvent(mouse);
             readEmulatorState();
             return;
@@ -678,10 +565,273 @@ public class TTerminalWidget extends TScrollableWidget
         super.onMouseMotion(mouse);
     }
 
+    /**
+     * Handle posted command events.
+     *
+     * @param command command event
+     */
+    @Override
+    public void onCommand(final TCommandEvent command) {
+        if (emulator == null) {
+            return;
+        }
+
+        if (command.equals(cmPaste)) {
+            // Paste text from clipboard.
+            String text = getClipboard().pasteText();
+            if (text != null) {
+                for (int i = 0; i < text.length(); ) {
+                    int ch = text.codePointAt(i);
+                    emulator.addUserEvent(new TKeypressEvent(false, 0, ch,
+                            false, false, false));
+                    i += Character.charCount(ch);
+                }
+            }
+            return;
+        }
+    }
+
+    // ------------------------------------------------------------------------
+    // TScrollableWidget ------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Draw the display buffer.
+     */
+    @Override
+    public void draw() {
+        if (emulator == null) {
+            return;
+        }
+
+        int width = getDisplayWidth();
+
+        boolean syncEmulator = false;
+        if (System.currentTimeMillis() - lastUpdateTime >= 50) {
+            // 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() {
+        if (emulator != null) {
+            emulator.close();
+        }
+        if (shell != null) {
+            terminateShellChildProcess();
+            shell.destroy();
+            shell = null;
+        }
+    }
+
+    /**
+     * Resize scrollbars for a new width/height.
+     */
+    @Override
+    public void reflowData() {
+        if (emulator == null) {
+            return;
+        }
+
+        // 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 --------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Check for 'ptypipe' on the path.  If available, set ptypipeOnPath.
+     */
+    private static void checkForPtypipe() {
+        String systemPath = System.getenv("PATH");
+        if (systemPath == null) {
+            return;
+        }
+
+        String [] paths = systemPath.split(File.pathSeparator);
+        if (paths == null) {
+            return;
+        }
+        if (paths.length == 0) {
+            return;
+        }
+        for (int i = 0; i < paths.length; i++) {
+            File path = new File(paths[i]);
+            if (path.exists() && path.isDirectory()) {
+                File [] files = path.listFiles();
+                if (files == null) {
+                    continue;
+                }
+                if (files.length == 0) {
+                    continue;
+                }
+                for (int j = 0; j < files.length; j++) {
+                    File file = files[j];
+                    if (file.canExecute() && file.getName().equals("ptypipe")) {
+                        ptypipeOnPath = true;
+                        return;
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * Get the desired window title.
      *
@@ -699,6 +849,9 @@ public class TTerminalWidget extends TScrollableWidget
      * cursor drawn over it
      */
     public boolean hasHiddenMouse() {
+        if (emulator == null) {
+            return false;
+        }
         return (emulator.hasHiddenMousePointer() || typingHidMouse);
     }
 
@@ -709,6 +862,9 @@ public class TTerminalWidget extends TScrollableWidget
      * side
      */
     public boolean isReading() {
+        if (emulator == null) {
+            return false;
+        }
         return emulator.isReading();
     }
 
@@ -820,17 +976,16 @@ public class TTerminalWidget extends TScrollableWidget
     public void onShellExit() {
         TApplication app = getApplication();
         if (app != null) {
-            app.invokeLater(new Runnable() {
-                public void run() {
-                    if (closeAction != 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));
-                    }
-                }
-            });
+                });
+            }
+            app.doRepaint();
         }
     }
 
@@ -839,6 +994,10 @@ public class TTerminalWidget extends TScrollableWidget
      * screen.
      */
     private void readEmulatorState() {
+        if (emulator == null) {
+            return;
+        }
+
         // Synchronize against the emulator so we don't stomp on its reader
         // thread.
         synchronized (emulator) {
@@ -896,6 +1055,19 @@ public class TTerminalWidget extends TScrollableWidget
         } // synchronized (emulator)
     }
 
+    /**
+     * 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 (emulator == null) {
+            return false;
+        }
+        return emulator.waitForOutput(millis);
+    }
+
     /**
      * Check if a mouse press/release/motion event coordinate is over the
      * emulator.
@@ -904,6 +1076,9 @@ public class TTerminalWidget extends TScrollableWidget
      * @return whether or not the mouse is on the emulator
      */
     private boolean mouseOnEmulator(final TMouseEvent mouse) {
+        if (emulator == null) {
+            return false;
+        }
 
         if (!emulator.isReading()) {
             return false;
@@ -919,20 +1094,6 @@ public class TTerminalWidget extends TScrollableWidget
         return false;
     }
 
-    /**
-     * Copy a display buffer.
-     *
-     * @param buffer the buffer to copy
-     * @return a deep copy of the buffer's data
-     */
-    private List<DisplayLine> copyBuffer(final List<DisplayLine> buffer) {
-        ArrayList<DisplayLine> result = new ArrayList<DisplayLine>(buffer.size());
-        for (DisplayLine line: buffer) {
-            result.add(new DisplayLine(line));
-        }
-        return result;
-    }
-
     /**
      * Draw glyphs for a double-width or double-height VT100 cell to two
      * screen cells.
@@ -1075,7 +1236,17 @@ public class TTerminalWidget extends TScrollableWidget
      * Called by emulator when fresh data has come in.
      */
     public void displayChanged() {
-        dirty = true;
+        if (emulator != null) {
+            // Force sync here: EMCA48.run() thread might be setting
+            // dirty=true while TTerminalWdiget.draw() is setting
+            // dirty=false.  If these writes start interleaving, the display
+            // stops getting updated.
+            synchronized (emulator) {
+                dirty = true;
+            }
+        } else {
+            dirty = true;
+        }
         getApplication().postEvent(new TMenuEvent(TMenu.MID_REPAINT));
     }
 
@@ -1103,4 +1274,53 @@ public class TTerminalWidget extends TScrollableWidget
         return 24;
     }
 
+    /**
+     * Get the exit value for the emulator.
+     *
+     * @return exit value
+     */
+    public int getExitValue() {
+        return exitValue;
+    }
+
+    // ------------------------------------------------------------------------
+    // EditMenuUser -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Check if the cut menu item should be enabled.
+     *
+     * @return true if the cut menu item should be enabled
+     */
+    public boolean isEditMenuCut() {
+        return false;
+    }
+
+    /**
+     * Check if the copy menu item should be enabled.
+     *
+     * @return true if the copy menu item should be enabled
+     */
+    public boolean isEditMenuCopy() {
+        return false;
+    }
+
+    /**
+     * Check if the paste menu item should be enabled.
+     *
+     * @return true if the paste menu item should be enabled
+     */
+    public boolean isEditMenuPaste() {
+        return true;
+    }
+
+    /**
+     * Check if the clear menu item should be enabled.
+     *
+     * @return true if the clear menu item should be enabled
+     */
+    public boolean isEditMenuClear() {
+        return false;
+    }
+
 }