Better resizing under ptypipe
[fanfix.git] / src / jexer / tterminal / ECMA48.java
index 7869829753df190c64306b51583731f3d812faec..96f4e4083cc0bba8085d2df1311492214142017d 100644 (file)
@@ -3,7 +3,7 @@
  *
  * The MIT License (MIT)
  *
- * Copyright (C) 2016 Kevin Lamonte
+ * Copyright (C) 2017 Kevin Lamonte
  *
  * Permission is hereby granted, free of charge, to any person obtaining a
  * copy of this software and associated documentation files (the "Software"),
@@ -28,6 +28,7 @@
  */
 package jexer.tterminal;
 
+import java.io.BufferedOutputStream;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.IOException;
@@ -46,10 +47,12 @@ import jexer.event.TMouseEvent;
 import jexer.bits.Color;
 import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
+import jexer.io.ReadTimeoutException;
+import jexer.io.TimeoutInputStream;
 import static jexer.TKeypress.*;
 
 /**
- * This implements a complex ANSI ECMA-48/ISO 6429/ANSI X3.64 type consoles,
+ * This implements a complex ECMA-48/ISO 6429/ANSI X3.64 type console,
  * including a scrollback buffer.
  *
  * <p>
@@ -129,15 +132,13 @@ public class ECMA48 implements Runnable {
             return "\033[?6c";
 
         case VT220:
+        case XTERM:
             // "I am a VT220" - 7 bit version
             if (!s8c1t) {
                 return "\033[?62;1;6c";
             }
             // "I am a VT220" - 8 bit version
             return "\u009b?62;1;6c";
-        case XTERM:
-            // "I am a VT100 with advanced video option" (often VT102)
-            return "\033[?1;2c";
         default:
             throw new IllegalArgumentException("Invalid device type: " + type);
         }
@@ -203,7 +204,7 @@ public class ECMA48 implements Runnable {
      *
      * @param str string to send
      */
-    private void writeRemote(final String str) {
+    public void writeRemote(final String str) {
         if (stopReaderThread) {
             // Reader hit EOF, bail out now.
             close();
@@ -220,6 +221,7 @@ public class ECMA48 implements Runnable {
                 return;
             }
             try {
+                outputStream.flush();
                 for (int i = 0; i < str.length(); i++) {
                     outputStream.write(str.charAt(i));
                 }
@@ -234,6 +236,7 @@ public class ECMA48 implements Runnable {
                 return;
             }
             try {
+                output.flush();
                 output.write(str);
                 output.flush();
             } catch (IOException e) {
@@ -252,77 +255,60 @@ public class ECMA48 implements Runnable {
      */
     public final void close() {
 
-        // Synchronize so we don't stomp on the reader thread.
-        synchronized (this) {
-
-            // Close the input stream
-            switch (type) {
-            case VT100:
-            case VT102:
-            case VT220:
-                if (inputStream != null) {
-                    try {
-                        inputStream.close();
-                    } catch (IOException e) {
-                        // SQUASH
-                    }
-                    inputStream = null;
-                }
-                break;
-            case XTERM:
-                if (input != null) {
-                    try {
-                        input.close();
-                    } catch (IOException e) {
-                        // SQUASH
-                    }
-                    input = null;
-                    inputStream = null;
-                }
-                break;
+        // Tell the reader thread to stop looking at input.  It will close
+        // the input streams.
+        if (stopReaderThread == false) {
+            stopReaderThread = true;
+            try {
+                readerThread.join(1000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
             }
+        }
 
-            // Tell the reader thread to stop looking at input.
-            if (stopReaderThread == false) {
-                stopReaderThread = true;
+        // Now close the output stream.
+        switch (type) {
+        case VT100:
+        case VT102:
+        case VT220:
+            if (outputStream != null) {
                 try {
-                    readerThread.join();
-                } catch (InterruptedException e) {
-                    e.printStackTrace();
+                    outputStream.close();
+                } catch (IOException e) {
+                    // SQUASH
                 }
+                outputStream = null;
             }
-
-            // 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
                 }
-                break;
-            case XTERM:
-                if (output != null) {
-                    try {
-                        output.close();
-                    } catch (IOException e) {
-                        // SQUASH
-                    }
-                    output = null;
+                outputStream = null;
+            }
+            if (output != null) {
+                try {
+                    output.close();
+                } catch (IOException e) {
+                    // SQUASH
                 }
-                break;
-            default:
-                throw new IllegalArgumentException("Invalid device type: "
-                    + type);
+                output = null;
             }
-        } // synchronized (this)
+            break;
+        default:
+            throw new IllegalArgumentException("Invalid device type: " +
+                type);
+        }
     }
 
+    /**
+     * The enclosing listening object.
+     */
+    private DisplayListener displayListener;
+
     /**
      * When true, the reader thread is expected to exit.
      */
@@ -395,7 +381,7 @@ public class ECMA48 implements Runnable {
     /**
      * The terminal's raw InputStream.  This is used for type != XTERM.
      */
-    private volatile InputStream inputStream;
+    private volatile TimeoutInputStream inputStream;
 
     /**
      * The terminal's output.  For type == XTERM, this wraps an
@@ -544,6 +530,22 @@ public class ECMA48 implements Runnable {
         return width;
     }
 
+    /**
+     * Set the display width.
+     *
+     * @param width the new width
+     */
+    public final 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;
+        }
+    }
+
     /**
      * Physical display height.  We start at 80x24, but the user can resize
      * us bigger/smaller.
@@ -559,6 +561,35 @@ public class ECMA48 implements Runnable {
         return height;
     }
 
+    /**
+     * Set the display height.
+     *
+     * @param height the new height
+     */
+    public final void setHeight(final int height) {
+        this.height = height;
+        if (scrollRegionBottom >= height) {
+            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) {
+            scrollback.add(display.remove(0));
+        }
+    }
+
     /**
      * Top margin of the scrolling region.
      */
@@ -867,6 +898,11 @@ public class ECMA48 implements Runnable {
         arrowKeyMode            = ArrowKeyMode.ANSI;
         keypadMode              = KeypadMode.Numeric;
         wrapLineFlag            = false;
+        if (displayListener != null) {
+            width = displayListener.getDisplayWidth();
+            height = displayListener.getDisplayHeight();
+            rightMargin         = width - 1;
+        }
 
         // Flags
         shiftOut                = false;
@@ -905,11 +941,14 @@ public class ECMA48 implements Runnable {
      * @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) throws UnsupportedEncodingException {
+        final OutputStream outputStream, final DisplayListener displayListener)
+        throws UnsupportedEncodingException {
 
         assert (inputStream != null);
         assert (outputStream != null);
@@ -920,15 +959,21 @@ public class ECMA48 implements Runnable {
         display           = new LinkedList<DisplayLine>();
 
         this.type         = type;
-        this.inputStream  = inputStream;
+        if (inputStream instanceof TimeoutInputStream) {
+            this.inputStream  = (TimeoutInputStream)inputStream;
+        } else {
+            this.inputStream  = new TimeoutInputStream(inputStream, 2000);
+        }
         if (type == DeviceType.XTERM) {
-            this.input    = new InputStreamReader(inputStream, "UTF-8");
-            this.output   = new OutputStreamWriter(outputStream, "UTF-8");
+            this.input    = new InputStreamReader(this.inputStream, "UTF-8");
+            this.output   = new OutputStreamWriter(new
+                BufferedOutputStream(outputStream), "UTF-8");
             this.outputStream = null;
         } else {
             this.output       = null;
-            this.outputStream = outputStream;
+            this.outputStream = new BufferedOutputStream(outputStream);
         }
+        this.displayListener  = displayListener;
 
         reset();
         for (int i = 0; i < height; i++) {
@@ -1814,20 +1859,9 @@ public class ECMA48 implements Runnable {
         if (keypress.equalsWithoutModifiers(kbPgUp)) {
             switch (type) {
             case XTERM:
-                switch (arrowKeyMode) {
-                case ANSI:
-                    return xtermBuildKeySequence("\033[", '5', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                case VT52:
-                    return xtermBuildKeySequence("\033", '5', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                case VT100:
-                    return xtermBuildKeySequence("\033O", '5', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                }
+                return xtermBuildKeySequence("\033[", '5', '~',
+                    keypress.isCtrl(), keypress.isAlt(),
+                    keypress.isShift());
             default:
                 return "\033[5~";
             }
@@ -1836,20 +1870,9 @@ public class ECMA48 implements Runnable {
         if (keypress.equalsWithoutModifiers(kbPgDn)) {
             switch (type) {
             case XTERM:
-                switch (arrowKeyMode) {
-                case ANSI:
-                    return xtermBuildKeySequence("\033[", '6', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                case VT52:
-                    return xtermBuildKeySequence("\033", '6', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                case VT100:
-                    return xtermBuildKeySequence("\033O", '6', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                }
+                return xtermBuildKeySequence("\033[", '6', '~',
+                    keypress.isCtrl(), keypress.isAlt(),
+                    keypress.isShift());
             default:
                 return "\033[6~";
             }
@@ -1858,20 +1881,9 @@ public class ECMA48 implements Runnable {
         if (keypress.equalsWithoutModifiers(kbIns)) {
             switch (type) {
             case XTERM:
-                switch (arrowKeyMode) {
-                case ANSI:
-                    return xtermBuildKeySequence("\033[", '2', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                case VT52:
-                    return xtermBuildKeySequence("\033", '2', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                case VT100:
-                    return xtermBuildKeySequence("\033O", '2', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                }
+                return xtermBuildKeySequence("\033[", '2', '~',
+                    keypress.isCtrl(), keypress.isAlt(),
+                    keypress.isShift());
             default:
                 return "\033[2~";
             }
@@ -1880,20 +1892,9 @@ public class ECMA48 implements Runnable {
         if (keypress.equalsWithoutModifiers(kbDel)) {
             switch (type) {
             case XTERM:
-                switch (arrowKeyMode) {
-                case ANSI:
-                    return xtermBuildKeySequence("\033[", '3', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                case VT52:
-                    return xtermBuildKeySequence("\033", '3', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                case VT100:
-                    return xtermBuildKeySequence("\033O", '3', '~',
-                        keypress.isCtrl(), keypress.isAlt(),
-                        keypress.isShift());
-                }
+                return xtermBuildKeySequence("\033[", '3', '~',
+                    keypress.isCtrl(), keypress.isAlt(),
+                    keypress.isShift());
             default:
                 // Delete sends real delete for VTxxx
                 return "\177";
@@ -2503,9 +2504,18 @@ public class ECMA48 implements Runnable {
                     } else {
                         // 80 columns
                         columns132 = false;
-                        rightMargin = 79;
+                        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;
+                        }
                     }
-                    width = rightMargin + 1;
                     // Entire screen is cleared, and scrolling region is
                     // reset
                     eraseScreen(0, 0, height - 1, width - 1, false);
@@ -3760,6 +3770,7 @@ public class ECMA48 implements Runnable {
      */
     private void dsr() {
         boolean decPrivateModeFlag = false;
+        int row = currentState.cursorY;
 
         for (int i = 0; i < collectBuffer.length(); i++) {
             if (collectBuffer.charAt(i) == '?') {
@@ -3787,15 +3798,18 @@ public class ECMA48 implements Runnable {
 
         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",
-                    currentState.cursorY + 1, currentState.cursorX + 1);
+                str = String.format("\u009b%d;%dR", row + 1,
+                    currentState.cursorX + 1);
             } else {
-                str = String.format("\033[%d;%dR",
-                    currentState.cursorY + 1, currentState.cursorX + 1);
+                str = String.format("\033[%d;%dR", row + 1,
+                    currentState.cursorX + 1);
             }
 
             // Send string directly to remote side
@@ -6023,11 +6037,13 @@ public class ECMA48 implements Runnable {
         while (!done && !stopReaderThread) {
             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);
+                        int newSizeHalf = Math.max(readBufferUTF8.length,
+                            n);
 
                         readBufferUTF8 = new char[newSizeHalf * 2];
                     }
@@ -6038,15 +6054,28 @@ public class ECMA48 implements Runnable {
                         readBuffer = new byte[newSizeHalf * 2];
                     }
                 }
+                if (n == 0) {
+                    try {
+                        Thread.sleep(2);
+                    } catch (InterruptedException e) {
+                        // SQUASH
+                    }
+                    continue;
+                }
 
                 int rc = -1;
-                if (utf8) {
-                    rc = input.read(readBufferUTF8, 0,
-                        readBufferUTF8.length);
-                } else {
-                    rc = inputStream.read(readBuffer, 0,
-                        readBuffer.length);
+                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
@@ -6059,22 +6088,42 @@ public class ECMA48 implements Runnable {
                         } else {
                             ch = readBuffer[i];
                         }
-                        // Don't step on UI events
+
                         synchronized (this) {
+                            // Don't step on UI events
                             consume((char)ch);
                         }
                     }
+                    // 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) {
                 e.printStackTrace();
                 done = true;
             }
+
         } // 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
+        }
+
         // System.err.println("*** run() exiting..."); System.err.flush();
     }