| 1 | package com.googlecode.lanterna.terminal.ansi; |
| 2 | |
| 3 | import java.io.BufferedReader; |
| 4 | import java.io.ByteArrayInputStream; |
| 5 | import java.io.ByteArrayOutputStream; |
| 6 | import java.io.File; |
| 7 | import java.io.IOException; |
| 8 | import java.io.InputStream; |
| 9 | import java.io.InputStreamReader; |
| 10 | import java.io.OutputStream; |
| 11 | import java.lang.reflect.InvocationHandler; |
| 12 | import java.lang.reflect.Method; |
| 13 | import java.lang.reflect.Proxy; |
| 14 | import java.nio.charset.Charset; |
| 15 | |
| 16 | import com.googlecode.lanterna.input.KeyStroke; |
| 17 | |
| 18 | /** |
| 19 | * UnixLikeTerminal extends from ANSITerminal and defines functionality that is common to |
| 20 | * {@code UnixTerminal} and {@code CygwinTerminal}, like setting tty modes; echo, cbreak |
| 21 | * and minimum characters for reading as well as a shutdown hook to set the tty back to |
| 22 | * original state at the end. |
| 23 | * <p> |
| 24 | * If requested, it handles Control-C input to terminate the program, and hooks |
| 25 | * into Unix WINCH signal to detect when the user has resized the terminal, |
| 26 | * if supported by the JVM. |
| 27 | * |
| 28 | * @author Andreas |
| 29 | * @author Martin |
| 30 | */ |
| 31 | public abstract class UnixLikeTerminal extends ANSITerminal { |
| 32 | |
| 33 | /** |
| 34 | * This enum lets you control how Lanterna will handle a ctrl+c keystroke from the user. |
| 35 | */ |
| 36 | public enum CtrlCBehaviour { |
| 37 | /** |
| 38 | * Pressing ctrl+c doesn't kill the application, it will be added to the input queue as any other key stroke |
| 39 | */ |
| 40 | TRAP, |
| 41 | /** |
| 42 | * Pressing ctrl+c will restore the terminal and kill the application as it normally does with terminal |
| 43 | * applications. Lanterna will restore the terminal and then call {@code System.exit(1)} for this. |
| 44 | */ |
| 45 | CTRL_C_KILLS_APPLICATION, |
| 46 | } |
| 47 | |
| 48 | protected final CtrlCBehaviour terminalCtrlCBehaviour; |
| 49 | protected final File ttyDev; |
| 50 | private String sttyStatusToRestore; |
| 51 | |
| 52 | /** |
| 53 | * Creates a UnixTerminal using a specified input stream, output stream and character set, with a custom size |
| 54 | * querier instead of using the default one. This way you can override size detection (if you want to force the |
| 55 | * terminal to a fixed size, for example). You also choose how you want ctrl+c key strokes to be handled. |
| 56 | * |
| 57 | * @param terminalInput Input stream to read terminal input from |
| 58 | * @param terminalOutput Output stream to write terminal output to |
| 59 | * @param terminalCharset Character set to use when converting characters to bytes |
| 60 | * @param terminalCtrlCBehaviour Special settings on how the terminal will behave, see {@code UnixTerminalMode} for more |
| 61 | * details |
| 62 | * @param ttyDev File to redirect standard input from in exec(), if not null. |
| 63 | */ |
| 64 | @SuppressWarnings({"SameParameterValue", "WeakerAccess"}) |
| 65 | public UnixLikeTerminal( |
| 66 | InputStream terminalInput, |
| 67 | OutputStream terminalOutput, |
| 68 | Charset terminalCharset, |
| 69 | CtrlCBehaviour terminalCtrlCBehaviour, |
| 70 | File ttyDev) { |
| 71 | super(terminalInput, terminalOutput, terminalCharset); |
| 72 | this.terminalCtrlCBehaviour = terminalCtrlCBehaviour; |
| 73 | this.sttyStatusToRestore = null; |
| 74 | this.ttyDev = ttyDev; |
| 75 | } |
| 76 | |
| 77 | protected String exec(String... cmd) throws IOException { |
| 78 | if (ttyDev != null) { |
| 79 | //Here's what we try to do, but that is Java 7+ only: |
| 80 | // processBuilder.redirectInput(ProcessBuilder.Redirect.from(ttyDev)); |
| 81 | //instead, for Java 6, we join the cmd into a scriptlet with redirection |
| 82 | //and replace cmd by a call to sh with the scriptlet: |
| 83 | StringBuilder sb = new StringBuilder(); |
| 84 | for (String arg : cmd) { sb.append(arg).append(' '); } |
| 85 | sb.append("< ").append(ttyDev); |
| 86 | cmd = new String[] { "sh", "-c", sb.toString() }; |
| 87 | } |
| 88 | ProcessBuilder pb = new ProcessBuilder(cmd); |
| 89 | Process process = pb.start(); |
| 90 | ByteArrayOutputStream stdoutBuffer = new ByteArrayOutputStream(); |
| 91 | InputStream stdout = process.getInputStream(); |
| 92 | int readByte = stdout.read(); |
| 93 | while(readByte >= 0) { |
| 94 | stdoutBuffer.write(readByte); |
| 95 | readByte = stdout.read(); |
| 96 | } |
| 97 | ByteArrayInputStream stdoutBufferInputStream = new ByteArrayInputStream(stdoutBuffer.toByteArray()); |
| 98 | BufferedReader reader = new BufferedReader(new InputStreamReader(stdoutBufferInputStream)); |
| 99 | StringBuilder builder = new StringBuilder(); |
| 100 | String line; |
| 101 | while((line = reader.readLine()) != null) { |
| 102 | builder.append(line); |
| 103 | } |
| 104 | reader.close(); |
| 105 | return builder.toString(); |
| 106 | } |
| 107 | |
| 108 | @Override |
| 109 | public KeyStroke pollInput() throws IOException { |
| 110 | //Check if we have ctrl+c coming |
| 111 | KeyStroke key = super.pollInput(); |
| 112 | isCtrlC(key); |
| 113 | return key; |
| 114 | } |
| 115 | |
| 116 | @Override |
| 117 | public KeyStroke readInput() throws IOException { |
| 118 | //Check if we have ctrl+c coming |
| 119 | KeyStroke key = super.readInput(); |
| 120 | isCtrlC(key); |
| 121 | return key; |
| 122 | } |
| 123 | |
| 124 | private void isCtrlC(KeyStroke key) throws IOException { |
| 125 | if(key != null |
| 126 | && terminalCtrlCBehaviour == CtrlCBehaviour.CTRL_C_KILLS_APPLICATION |
| 127 | && key.getCharacter() != null |
| 128 | && key.getCharacter() == 'c' |
| 129 | && !key.isAltDown() |
| 130 | && key.isCtrlDown()) { |
| 131 | |
| 132 | exitPrivateMode(); |
| 133 | System.exit(1); |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | protected void setupWinResizeHandler() { |
| 138 | try { |
| 139 | Class<?> signalClass = Class.forName("sun.misc.Signal"); |
| 140 | for(Method m : signalClass.getDeclaredMethods()) { |
| 141 | if("handle".equals(m.getName())) { |
| 142 | Object windowResizeHandler = Proxy.newProxyInstance(getClass().getClassLoader(), new Class[]{Class.forName("sun.misc.SignalHandler")}, new InvocationHandler() { |
| 143 | @Override |
| 144 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { |
| 145 | if("handle".equals(method.getName())) { |
| 146 | getTerminalSize(); |
| 147 | } |
| 148 | return null; |
| 149 | } |
| 150 | }); |
| 151 | m.invoke(null, signalClass.getConstructor(String.class).newInstance("WINCH"), windowResizeHandler); |
| 152 | } |
| 153 | } |
| 154 | } catch(Throwable e) { |
| 155 | System.err.println(e.getMessage()); |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | protected void setupShutdownHook() { |
| 160 | Runtime.getRuntime().addShutdownHook(new Thread("Lanterna STTY restore") { |
| 161 | @Override |
| 162 | public void run() { |
| 163 | try { |
| 164 | if (isInPrivateMode()) { |
| 165 | exitPrivateMode(); |
| 166 | } |
| 167 | } |
| 168 | catch(IOException ignored) {} |
| 169 | catch(IllegalStateException ignored) {} // still possible! |
| 170 | |
| 171 | try { |
| 172 | restoreSTTY(); |
| 173 | } |
| 174 | catch(IOException ignored) {} |
| 175 | } |
| 176 | }); |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * Enabling cbreak mode will allow you to read user input immediately as the user enters the characters, as opposed |
| 181 | * to reading the data in lines as the user presses enter. If you want your program to respond to user input by the |
| 182 | * keyboard, you probably want to enable cbreak mode. |
| 183 | * |
| 184 | * @see <a href="http://en.wikipedia.org/wiki/POSIX_terminal_interface">POSIX terminal interface</a> |
| 185 | * @param cbreakOn Should cbreak be turned on or not |
| 186 | * @throws IOException |
| 187 | */ |
| 188 | public void setCBreak(boolean cbreakOn) throws IOException { |
| 189 | sttyICanon(!cbreakOn); |
| 190 | } |
| 191 | |
| 192 | /** |
| 193 | * Enables or disables keyboard echo, meaning the immediate output of the characters you type on your keyboard. If |
| 194 | * your users are going to interact with this application through the keyboard, you probably want to disable echo |
| 195 | * mode. |
| 196 | * |
| 197 | * @param echoOn true if keyboard input will immediately echo, false if it's hidden |
| 198 | * @throws IOException |
| 199 | */ |
| 200 | public void setEcho(boolean echoOn) throws IOException { |
| 201 | sttyKeyEcho(echoOn); |
| 202 | } |
| 203 | |
| 204 | protected void saveSTTY() throws IOException { |
| 205 | if(sttyStatusToRestore == null) { |
| 206 | sttyStatusToRestore = sttySave(); |
| 207 | } |
| 208 | } |
| 209 | |
| 210 | protected synchronized void restoreSTTY() throws IOException { |
| 211 | if(sttyStatusToRestore != null) { |
| 212 | sttyRestore( sttyStatusToRestore ); |
| 213 | sttyStatusToRestore = null; |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | // A couple of system-dependent helpers: |
| 218 | protected abstract void sttyKeyEcho(final boolean enable) throws IOException; |
| 219 | protected abstract void sttyMinimum1CharacterForRead() throws IOException; |
| 220 | protected abstract void sttyICanon(final boolean enable) throws IOException; |
| 221 | protected abstract String sttySave() throws IOException; |
| 222 | protected abstract void sttyRestore(String tok) throws IOException; |
| 223 | |
| 224 | } |