From 5dfd1c11947e9cb32fcac4772f1b16879d9ffe67 Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Wed, 15 Mar 2017 18:12:56 -0400 Subject: [PATCH] Bug fixes --- README.md | 9 +- build.xml | 2 +- src/jexer/TApplication.java | 46 ++-- src/jexer/TTerminalWindow.java | 81 +++++- src/jexer/TText.java | 56 ++-- src/jexer/TWindow.java | 44 +++ src/jexer/io/ECMA48Screen.java | 2 +- src/jexer/io/ReadTimeoutException.java | 47 ++++ src/jexer/io/TimeoutInputStream.java | 365 +++++++++++++++++++++++++ src/jexer/menu/TMenu.java | 2 +- src/jexer/menu/TMenuItem.java | 2 +- src/jexer/tterminal/ECMA48.java | 159 ++++++----- 12 files changed, 677 insertions(+), 138 deletions(-) create mode 100644 src/jexer/io/ReadTimeoutException.java create mode 100644 src/jexer/io/TimeoutInputStream.java diff --git a/README.md b/README.md index 4e71a9c..ab6069e 100644 --- a/README.md +++ b/README.md @@ -172,12 +172,6 @@ Some arbitrary design decisions had to be made when either the obviously expected behavior did not happen or when a specification was ambiguous. This section describes such issues. - - TTerminalWindow will hang on input from the remote if the - TApplication is exited before the TTerminalWindow's process has - closed on its own. This is due to a Java limitation/interaction - between blocking reads (which is necessary to get UTF8 translation - correct) and file streams. - - See jexer.tterminal.ECMA48 for more specifics of terminal emulation limitations. @@ -191,6 +185,9 @@ ambiguous. This section describes such issues. checking for a tty: script launches $SHELL in a pseudo-tty. This works on Linux but might not on other Posix-y platforms. + - Closing a TTerminalWindow without exiting the process inside it + may result in a zombie 'script' process. + - Java's InputStreamReader as used by the ECMA48 backend requires a valid UTF-8 stream. The default X10 encoding for mouse coordinates outside (160,94) can corrupt that stream, at best diff --git a/build.xml b/build.xml index 0609921..45014d9 100644 --- a/build.xml +++ b/build.xml @@ -30,7 +30,7 @@ - + diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index 0a2ab19..971f6f8 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -965,20 +965,35 @@ public class TApplication implements Runnable { if (event instanceof TKeypressEvent) { TKeypressEvent keypress = (TKeypressEvent) event; - // See if this key matches an accelerator, and if so dispatch the - // menu event. - TKeypress keypressLowercase = keypress.getKey().toLowerCase(); - TMenuItem item = null; - synchronized (accelerators) { - item = accelerators.get(keypressLowercase); + // See if this key matches an accelerator, and is not being + // shortcutted by the active window, and if so dispatch the menu + // event. + boolean windowWillShortcut = false; + for (TWindow window: windows) { + if (window.isActive()) { + if (window.isShortcutKeypress(keypress.getKey())) { + // We do not process this key, it will be passed to + // the window instead. + windowWillShortcut = true; + } + } } - if (item != null) { - if (item.isEnabled()) { - // Let the menu item dispatch - item.dispatch(); - return; + + if (!windowWillShortcut) { + TKeypress keypressLowercase = keypress.getKey().toLowerCase(); + TMenuItem item = null; + synchronized (accelerators) { + item = accelerators.get(keypressLowercase); + } + if (item != null) { + if (item.isEnabled()) { + // Let the menu item dispatch + item.dispatch(); + return; + } } } + // Handle the keypress if (onKeypress(keypress)) { return; @@ -1659,7 +1674,7 @@ public class TApplication implements Runnable { * * @param event new event to add to the queue */ - public final void addMenuEvent(final TInputEvent event) { + public final void postMenuEvent(final TInputEvent event) { synchronized (fillEventQueue) { fillEventQueue.add(event); } @@ -1745,11 +1760,8 @@ public class TApplication implements Runnable { if (activeMenu != null) { return; } - - synchronized (windows) { - for (TWindow window: windows) { - closeWindow(window); - } + while (windows.size() > 0) { + closeWindow(windows.get(0)); } } diff --git a/src/jexer/TTerminalWindow.java b/src/jexer/TTerminalWindow.java index 8730dfe..6d97531 100644 --- a/src/jexer/TTerminalWindow.java +++ b/src/jexer/TTerminalWindow.java @@ -65,6 +65,76 @@ public class TTerminalWindow extends TWindow { */ private TVScroller vScroller; + /** + * Claim the keystrokes the emulator will need. + */ + private void addShortcutKeys() { + addShortcutKeypress(kbCtrlA); + addShortcutKeypress(kbCtrlB); + addShortcutKeypress(kbCtrlC); + addShortcutKeypress(kbCtrlD); + addShortcutKeypress(kbCtrlE); + addShortcutKeypress(kbCtrlF); + addShortcutKeypress(kbCtrlG); + addShortcutKeypress(kbCtrlH); + addShortcutKeypress(kbCtrlU); + addShortcutKeypress(kbCtrlJ); + addShortcutKeypress(kbCtrlK); + addShortcutKeypress(kbCtrlL); + addShortcutKeypress(kbCtrlM); + addShortcutKeypress(kbCtrlN); + addShortcutKeypress(kbCtrlO); + addShortcutKeypress(kbCtrlP); + addShortcutKeypress(kbCtrlQ); + addShortcutKeypress(kbCtrlR); + addShortcutKeypress(kbCtrlS); + addShortcutKeypress(kbCtrlT); + addShortcutKeypress(kbCtrlU); + addShortcutKeypress(kbCtrlV); + addShortcutKeypress(kbCtrlW); + addShortcutKeypress(kbCtrlX); + addShortcutKeypress(kbCtrlY); + addShortcutKeypress(kbCtrlZ); + addShortcutKeypress(kbF1); + addShortcutKeypress(kbF2); + addShortcutKeypress(kbF3); + addShortcutKeypress(kbF4); + addShortcutKeypress(kbF5); + addShortcutKeypress(kbF6); + addShortcutKeypress(kbF7); + addShortcutKeypress(kbF8); + addShortcutKeypress(kbF9); + addShortcutKeypress(kbF10); + addShortcutKeypress(kbF11); + addShortcutKeypress(kbF12); + addShortcutKeypress(kbAltA); + addShortcutKeypress(kbAltB); + addShortcutKeypress(kbAltC); + addShortcutKeypress(kbAltD); + addShortcutKeypress(kbAltE); + addShortcutKeypress(kbAltF); + addShortcutKeypress(kbAltG); + addShortcutKeypress(kbAltH); + addShortcutKeypress(kbAltU); + addShortcutKeypress(kbAltJ); + addShortcutKeypress(kbAltK); + addShortcutKeypress(kbAltL); + addShortcutKeypress(kbAltM); + addShortcutKeypress(kbAltN); + addShortcutKeypress(kbAltO); + addShortcutKeypress(kbAltP); + addShortcutKeypress(kbAltQ); + addShortcutKeypress(kbAltR); + addShortcutKeypress(kbAltS); + addShortcutKeypress(kbAltT); + addShortcutKeypress(kbAltU); + addShortcutKeypress(kbAltV); + addShortcutKeypress(kbAltW); + addShortcutKeypress(kbAltX); + addShortcutKeypress(kbAltY); + addShortcutKeypress(kbAltZ); + } + /** * Public constructor spawns a shell. * @@ -116,6 +186,9 @@ public class TTerminalWindow extends TWindow { // Setup the scroll bars onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), getHeight())); + + // Claim the keystrokes the emulator will need. + addShortcutKeys(); } /** @@ -144,6 +217,8 @@ public class TTerminalWindow extends TWindow { onResize(new TResizeEvent(TResizeEvent.Type.WIDGET, getWidth(), getHeight())); + // Claim the keystrokes the emulator will need. + addShortcutKeys(); } /** @@ -240,11 +315,11 @@ public class TTerminalWindow extends TWindow { * Handle window close. */ @Override public void onClose() { + emulator.close(); if (shell != null) { + // System.err.println("shell.destroy()"); shell.destroy(); shell = null; - } else { - emulator.close(); } } @@ -286,6 +361,7 @@ public class TTerminalWindow extends TWindow { getTitle(), rc)); shell = null; emulator.close(); + clearShortcutKeypresses(); } catch (IllegalThreadStateException e) { // The emulator thread has exited, but the shell Process // hasn't figured that out yet. Do nothing, we will see @@ -300,6 +376,7 @@ public class TTerminalWindow extends TWindow { getTitle(), rc)); shell = null; emulator.close(); + clearShortcutKeypresses(); } catch (IllegalThreadStateException e) { // The shell is still running, do nothing. } diff --git a/src/jexer/TText.java b/src/jexer/TText.java index 1750be1..007ff2d 100644 --- a/src/jexer/TText.java +++ b/src/jexer/TText.java @@ -88,8 +88,7 @@ public final class TText extends TWidget { /** * Convenience method used by TWindowLoggerOutput. * - * @param line - * new line to add + * @param line new line to add */ public void addLine(final String line) { if (text.length() == 0) { @@ -134,10 +133,8 @@ public final class TText extends TWidget { * the final string with a newline. Note that interior newlines are * converted to spaces. * - * @param str - * the string - * @param n - * the maximum number of characters in a line + * @param str the string + * @param n the maximum number of characters in a line * @return the wrapped string */ private String wrap(final String str, final int n) { @@ -221,18 +218,12 @@ public final class TText extends TWidget { /** * Public constructor. * - * @param parent - * parent widget - * @param text - * text on the screen - * @param x - * column relative to parent - * @param y - * row relative to parent - * @param width - * width of text area - * @param height - * height of text area + * @param parent parent widget + * @param text text on the screen + * @param x column relative to parent + * @param y row relative to parent + * @param width width of text area + * @param height height of text area */ public TText(final TWidget parent, final String text, final int x, final int y, final int width, final int height) { @@ -243,21 +234,14 @@ public final class TText extends TWidget { /** * Public constructor. * - * @param parent - * parent widget - * @param text - * text on the screen - * @param x - * column relative to parent - * @param y - * row relative to parent - * @param width - * width of text area - * @param height - * height of text area - * @param colorKey - * ColorTheme key color to use for foreground text. Default is - * "ttext" + * @param parent parent widget + * @param text text on the screen + * @param x column relative to parent + * @param y row relative to parent + * @param width width of text area + * @param height height of text area + * @param colorKey ColorTheme key color to use for foreground + * text. Default is "ttext". */ public TText(final TWidget parent, final String text, final int x, final int y, final int width, final int height, @@ -311,8 +295,7 @@ public final class TText extends TWidget { /** * Handle mouse press events. * - * @param mouse - * mouse button press event + * @param mouse mouse button press event */ @Override public void onMouseDown(final TMouseEvent mouse) { @@ -332,8 +315,7 @@ public final class TText extends TWidget { /** * Handle keystrokes. * - * @param keypress - * keystroke event + * @param keypress keystroke event */ @Override public void onKeypress(final TKeypressEvent keypress) { diff --git a/src/jexer/TWindow.java b/src/jexer/TWindow.java index 26bd8c4..7302695 100644 --- a/src/jexer/TWindow.java +++ b/src/jexer/TWindow.java @@ -28,6 +28,8 @@ */ package jexer; +import java.util.HashSet; + import jexer.bits.Cell; import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; @@ -137,6 +139,48 @@ public class TWindow extends TWidget { this.z = z; } + /** + * Window's keyboard shortcuts. Any key in this set will be passed to + * the window directly rather than processed through the menu + * accelerators. + */ + private HashSet keyboardShortcuts = new HashSet(); + + /** + * Add a keypress to be overridden for this window. + * + * @param key the key to start taking control of + */ + protected void addShortcutKeypress(final TKeypress key) { + keyboardShortcuts.add(key); + } + + /** + * Remove a keypress to be overridden for this window. + * + * @param key the key to stop taking control of + */ + protected void removeShortcutKeypress(final TKeypress key) { + keyboardShortcuts.remove(key); + } + + /** + * Remove all keypresses to be overridden for this window. + */ + protected void clearShortcutKeypresses() { + keyboardShortcuts.clear(); + } + + /** + * Determine if a keypress is overridden for this window. + * + * @param key the key to check + * @return true if this window wants to process this key on its own + */ + public boolean isShortcutKeypress(final TKeypress key) { + return keyboardShortcuts.contains(key); + } + /** * If true, then the user clicked on the title bar and is moving the * window. diff --git a/src/jexer/io/ECMA48Screen.java b/src/jexer/io/ECMA48Screen.java index 4c3af18..a00a180 100644 --- a/src/jexer/io/ECMA48Screen.java +++ b/src/jexer/io/ECMA48Screen.java @@ -118,7 +118,7 @@ public final class ECMA48Screen extends Screen { for (int i = x; i < width; i++) { assert (logical[i][y].isBlank()); - // Physical is always updatesd + // Physical is always updated physical[i][y].reset(); } diff --git a/src/jexer/io/ReadTimeoutException.java b/src/jexer/io/ReadTimeoutException.java new file mode 100644 index 0000000..0a1bbc5 --- /dev/null +++ b/src/jexer/io/ReadTimeoutException.java @@ -0,0 +1,47 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * 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"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.io; + +import java.io.IOException; + +/** + * ReadTimeoutException is thrown by TimeoutInputStream.read() when bytes are + * not available within the timeout specified. + */ +public class ReadTimeoutException extends IOException { + + /** + * Construct an instance with a message. + * + * @param msg exception text + */ + public ReadTimeoutException(String msg) { + super(msg); + } +} diff --git a/src/jexer/io/TimeoutInputStream.java b/src/jexer/io/TimeoutInputStream.java new file mode 100644 index 0000000..f1b140b --- /dev/null +++ b/src/jexer/io/TimeoutInputStream.java @@ -0,0 +1,365 @@ +/* + * Jexer - Java Text User Interface + * + * The MIT License (MIT) + * + * 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"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * @author Kevin Lamonte [kevin.lamonte@gmail.com] + * @version 1 + */ +package jexer.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * This class provides an optional millisecond timeout on its read() + * operations. This permits callers to bail out rather than block. + */ +public class TimeoutInputStream extends InputStream { + + /** + * The wrapped stream. + */ + private InputStream stream; + + /** + * The timeout value in millis. If it takes longer than this for bytes + * to be available for read then a ReadTimeoutException is thrown. A + * value of 0 means to block as a normal InputStream would. + */ + private int timeoutMillis; + + /** + * If true, the current read() will timeout soon. + */ + private volatile boolean cancel = false; + + /** + * Request that the current read() operation timeout immediately. + */ + public synchronized void cancelRead() { + cancel = true; + } + + /** + * Public constructor, at the default timeout of 10000 millis (10 + * seconds). + * + * @param stream the wrapped InputStream + */ + public TimeoutInputStream(final InputStream stream) { + this.stream = stream; + this.timeoutMillis = 10000; + } + + /** + * Public constructor, using the default 10 bits per byte. + * + * @param stream the wrapped InputStream + * @param timeoutMillis the timeout value in millis. If it takes longer + * than this for bytes to be available for read then a + * ReadTimeoutException is thrown. A value of 0 means to block as a + * normal InputStream would. + */ + public TimeoutInputStream(final InputStream stream, + final int timeoutMillis) { + + if (timeoutMillis < 0) { + throw new IllegalArgumentException("Invalid timeoutMillis value, " + + "must be >= 0"); + } + + this.stream = stream; + this.timeoutMillis = timeoutMillis; + } + + /** + * Reads the next byte of data from the input stream. + * + * @return the next byte of data, or -1 if there is no more data because + * the end of the stream has been reached. + * @throws IOException if an I/O error occurs + */ + @Override + public int read() throws IOException { + + if (timeoutMillis == 0) { + // Block on the read(). + return stream.read(); + } + + if (stream.available() > 0) { + // A byte is available now, return it. + return stream.read(); + } + + // We will wait up to timeoutMillis to see if a byte is available. + // If not, we throw ReadTimeoutException. + long checkTime = System.currentTimeMillis(); + while (stream.available() == 0) { + long now = System.currentTimeMillis(); + synchronized (this) { + if ((now - checkTime > timeoutMillis) || (cancel == true)) { + if (cancel == true) { + cancel = false; + } + throw new ReadTimeoutException("Timeout on read(): " + + (int) (now - checkTime) + " millis and still no data"); + } + } + try { + // How long do we sleep for, eh? For now we will go with 2 + // millis. + Thread.currentThread().sleep(2); + } catch (InterruptedException e) { + // SQUASH + } + } + + if (stream.available() > 0) { + // A byte is available now, return it. + return stream.read(); + } + + throw new IOException("InputStream claimed a byte was available, but " + + "now it is not. What is going on?"); + } + + /** + * Reads some number of bytes from the input stream and stores them into + * the buffer array b. + * + * @param b the buffer into which the data is read. + * @return the total number of bytes read into the buffer, or -1 if there + * is no more data because the end of the stream has been reached. + * @throws IOException if an I/O error occurs + */ + @Override + public int read(final byte[] b) throws IOException { + if (timeoutMillis == 0) { + // Block on the read(). + return stream.read(b); + } + + int remaining = b.length; + + if (stream.available() >= remaining) { + // Enough bytes are available now, return them. + return stream.read(b); + } + + while (remaining > 0) { + + // We will wait up to timeoutMillis to see if a byte is + // available. If not, we throw ReadTimeoutException. + long checkTime = System.currentTimeMillis(); + while (stream.available() == 0) { + long now = System.currentTimeMillis(); + + synchronized (this) { + if ((now - checkTime > timeoutMillis) || (cancel == true)) { + if (cancel == true) { + cancel = false; + } + throw new ReadTimeoutException("Timeout on read(): " + + (int) (now - checkTime) + " millis and still no " + + "data"); + } + } + try { + // How long do we sleep for, eh? For now we will go with + // 2 millis. + Thread.currentThread().sleep(2); + } catch (InterruptedException e) { + // SQUASH + } + } + + if (stream.available() > 0) { + // At least one byte is available now, read it. + int n = stream.available(); + if (remaining < n) { + n = remaining; + } + int rc = stream.read(b, b.length - remaining, n); + if (rc == -1) { + // This shouldn't happen. + throw new IOException("InputStream claimed bytes were " + + "available, but read() returned -1. What is going " + + "on?"); + } + remaining -= rc; + return rc; + } + } + + throw new IOException("InputStream claimed all bytes were available, " + + "but now it is not. What is going on?"); + } + + /** + * Reads up to len bytes of data from the input stream into an array of + * bytes. + * + * @param b the buffer into which the data is read. + * @param off the start offset in array b at which the data is written. + * @param len the maximum number of bytes to read. + * @return the total number of bytes read into the buffer, or -1 if there + * is no more data because the end of the stream has been reached. + * @throws IOException if an I/O error occurs + */ + @Override + public int read(final byte[] b, final int off, + final int len) throws IOException { + + if (timeoutMillis == 0) { + // Block on the read(). + return stream.read(b); + } + + int remaining = len; + + if (stream.available() >= remaining) { + // Enough bytes are available now, return them. + return stream.read(b, off, remaining); + } + + while (remaining > 0) { + + // We will wait up to timeoutMillis to see if a byte is + // available. If not, we throw ReadTimeoutException. + long checkTime = System.currentTimeMillis(); + while (stream.available() == 0) { + long now = System.currentTimeMillis(); + synchronized (this) { + if ((now - checkTime > timeoutMillis) || (cancel == true)) { + if (cancel == true) { + cancel = false; + } + throw new ReadTimeoutException("Timeout on read(): " + + (int) (now - checkTime) + " millis and still no " + + "data"); + } + } + try { + // How long do we sleep for, eh? For now we will go with + // 2 millis. + Thread.currentThread().sleep(2); + } catch (InterruptedException e) { + // SQUASH + } + } + + if (stream.available() > 0) { + // At least one byte is available now, read it. + int n = stream.available(); + if (remaining < n) { + n = remaining; + } + int rc = stream.read(b, off + len - remaining, n); + if (rc == -1) { + // This shouldn't happen. + throw new IOException("InputStream claimed bytes were " + + "available, but read() returned -1. What is going " + + "on?"); + } + return rc; + } + } + + throw new IOException("InputStream claimed all bytes were available, " + + "but now it is not. What is going on?"); + } + + /** + * Returns an estimate of the number of bytes that can be read (or + * skipped over) from this input stream without blocking by the next + * invocation of a method for this input stream. + * + * @return an estimate of the number of bytes that can be read (or + * skipped over) from this input stream without blocking or 0 when it + * reaches the end of the input stream. + * @throws IOException if an I/O error occurs + */ + @Override + public int available() throws IOException { + return stream.available(); + } + + /** + * Closes this input stream and releases any system resources associated + * with the stream. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void close() throws IOException { + stream.close(); + } + + /** + * Marks the current position in this input stream. + * + * @param readLimit the maximum limit of bytes that can be read before + * the mark position becomes invalid + */ + @Override + public void mark(final int readLimit) { + stream.mark(readLimit); + } + + /** + * Tests if this input stream supports the mark and reset methods. + * + * @return true if this stream instance supports the mark and reset + * methods; false otherwise + */ + @Override + public boolean markSupported() { + return stream.markSupported(); + } + + /** + * Repositions this stream to the position at the time the mark method + * was last called on this input stream. + * + * @throws IOException if an I/O error occurs + */ + @Override + public void reset() throws IOException { + stream.reset(); + } + + /** + * Skips over and discards n bytes of data from this input stream. + * + * @param n the number of bytes to be skipped + * @return the actual number of bytes skipped + * @throws IOException if an I/O error occurs + */ + @Override + public long skip(final long n) throws IOException { + return stream.skip(n); + } + +} diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java index 8915c0c..b0f99ae 100644 --- a/src/jexer/menu/TMenu.java +++ b/src/jexer/menu/TMenu.java @@ -436,7 +436,7 @@ public final class TMenu extends TWindow { break; case MID_WINDOW_CLOSE: label = "&Close"; - // key = kbCtrlW; + key = kbCtrlW; break; default: diff --git a/src/jexer/menu/TMenuItem.java b/src/jexer/menu/TMenuItem.java index ec2afb2..fe74c4e 100644 --- a/src/jexer/menu/TMenuItem.java +++ b/src/jexer/menu/TMenuItem.java @@ -255,7 +255,7 @@ public class TMenuItem extends TWidget { public void dispatch() { assert (isEnabled()); - getApplication().addMenuEvent(new TMenuEvent(id)); + getApplication().postMenuEvent(new TMenuEvent(id)); if (checkable) { checked = !checked; } diff --git a/src/jexer/tterminal/ECMA48.java b/src/jexer/tterminal/ECMA48.java index f657eaf..b3cb0ac 100644 --- a/src/jexer/tterminal/ECMA48.java +++ b/src/jexer/tterminal/ECMA48.java @@ -46,6 +46,8 @@ 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.*; /** @@ -250,75 +252,53 @@ 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); + } } /** @@ -393,7 +373,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 @@ -918,9 +898,13 @@ public class ECMA48 implements Runnable { display = new LinkedList(); 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.input = new InputStreamReader(this.inputStream, "UTF-8"); this.output = new OutputStreamWriter(outputStream, "UTF-8"); this.outputStream = null; } else { @@ -6025,11 +6009,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]; } @@ -6040,15 +6026,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 @@ -6061,8 +6060,9 @@ 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); } } @@ -6072,11 +6072,26 @@ public class ECMA48 implements Runnable { 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(); } -- 2.27.0