Commit | Line | Data |
---|---|---|
a3b510ab NR |
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 | } |