1
2
3
4
5
6
7 package jline;
8
9 import java.io.*;
10 import java.util.*;
11
12 /***
13 * <p>
14 * Terminal that is used for unix platforms. Terminal initialization
15 * is handled by issuing the <em>stty</em> command against the
16 * <em>/dev/tty</em> file to disable character echoing and enable
17 * character input. All known unix systems (including
18 * Linux and Macintosh OS X) support the <em>stty</em>), so this
19 * implementation should work for an reasonable POSIX system.
20 * </p>
21 *
22 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
23 * @author Updates <a href="mailto:dwkemp@gmail.com">Dale Kemp</a> 2005-12-03
24 */
25 public class UnixTerminal extends Terminal {
26 public static final short ARROW_START = 27;
27 public static final short ARROW_PREFIX = 91;
28 public static final short ARROW_LEFT = 68;
29 public static final short ARROW_RIGHT = 67;
30 public static final short ARROW_UP = 65;
31 public static final short ARROW_DOWN = 66;
32 public static final short O_PREFIX = 79;
33 public static final short HOME_CODE = 72;
34 public static final short END_CODE = 70;
35
36 public static final short DEL_THIRD = 51;
37 public static final short DEL_SECOND = 126;
38
39 private Map terminfo;
40 private boolean echoEnabled;
41 private String ttyConfig;
42 private boolean backspaceDeleteSwitched = false;
43 private static String sttyCommand =
44 System.getProperty("jline.sttyCommand", "stty");
45
46
47 String encoding = System.getProperty("input.encoding", "UTF-8");
48 ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding);
49 InputStreamReader replayReader;
50
51 public UnixTerminal() {
52 try {
53 replayReader = new InputStreamReader(replayStream, encoding);
54 } catch (Exception e) {
55 throw new RuntimeException(e);
56 }
57 }
58
59 protected void checkBackspace(){
60 String[] ttyConfigSplit = ttyConfig.split(":|=");
61
62 if (ttyConfigSplit.length < 7)
63 return;
64
65 if (ttyConfigSplit[6] == null)
66 return;
67
68 backspaceDeleteSwitched = ttyConfigSplit[6].equals("7f");
69 }
70
71 /***
72 * Remove line-buffered input by invoking "stty -icanon min 1"
73 * against the current terminal.
74 */
75 public void initializeTerminal() throws IOException, InterruptedException {
76
77 ttyConfig = stty("-g");
78
79
80 if ((ttyConfig.length() == 0)
81 || ((ttyConfig.indexOf("=") == -1)
82 && (ttyConfig.indexOf(":") == -1))) {
83 throw new IOException("Unrecognized stty code: " + ttyConfig);
84 }
85
86 checkBackspace();
87
88
89 stty("-icanon min 1");
90
91
92 stty("-echo");
93 echoEnabled = false;
94
95
96 try {
97 Runtime.getRuntime().addShutdownHook(new Thread() {
98 public void start() {
99 try {
100 restoreTerminal();
101 } catch (Exception e) {
102 consumeException(e);
103 }
104 }
105 });
106 } catch (AbstractMethodError ame) {
107
108 consumeException(ame);
109 }
110 }
111
112 /***
113 * Restore the original terminal configuration, which can be used when
114 * shutting down the console reader. The ConsoleReader cannot be
115 * used after calling this method.
116 */
117 public void restoreTerminal() throws Exception {
118 if (ttyConfig != null) {
119 stty(ttyConfig);
120 ttyConfig = null;
121 }
122 resetTerminal();
123 }
124
125
126
127 public int readVirtualKey(InputStream in) throws IOException {
128 int c = readCharacter(in);
129
130 if (backspaceDeleteSwitched)
131 if (c == DELETE)
132 c = '\b';
133 else if (c == '\b')
134 c = DELETE;
135
136
137
138
139 if (c == ARROW_START) {
140
141
142
143
144
145
146 while (c == ARROW_START)
147 c = readCharacter(in);
148 if (c == ARROW_PREFIX || c == O_PREFIX) {
149 c = readCharacter(in);
150 if (c == ARROW_UP) {
151 return CTRL_P;
152 } else if (c == ARROW_DOWN) {
153 return CTRL_N;
154 } else if (c == ARROW_LEFT) {
155 return CTRL_B;
156 } else if (c == ARROW_RIGHT) {
157 return CTRL_F;
158 } else if (c == HOME_CODE) {
159 return CTRL_A;
160 } else if (c == END_CODE) {
161 return CTRL_E;
162 } else if (c == DEL_THIRD) {
163 c = readCharacter(in);
164 return DELETE;
165 }
166 }
167 }
168
169 if (c > 128) {
170
171
172 replayStream.setInput(c, in);
173
174 c = replayReader.read();
175
176 }
177
178 return c;
179 }
180
181 /***
182 * No-op for exceptions we want to silently consume.
183 */
184 private void consumeException(Throwable e) {
185 }
186
187 public boolean isSupported() {
188 return true;
189 }
190
191 public boolean getEcho() {
192 return false;
193 }
194
195 /***
196 * Returns the value of "stty size" width param.
197 *
198 * <strong>Note</strong>: this method caches the value from the
199 * first time it is called in order to increase speed, which means
200 * that changing to size of the terminal will not be reflected
201 * in the console.
202 */
203 public int getTerminalWidth() {
204 int val = -1;
205
206 try {
207 val = getTerminalProperty("columns");
208 } catch (Exception e) {
209 }
210
211 if (val == -1) {
212 val = 80;
213 }
214
215 return val;
216 }
217
218 /***
219 * Returns the value of "stty size" height param.
220 *
221 * <strong>Note</strong>: this method caches the value from the
222 * first time it is called in order to increase speed, which means
223 * that changing to size of the terminal will not be reflected
224 * in the console.
225 */
226 public int getTerminalHeight() {
227 int val = -1;
228
229 try {
230 val = getTerminalProperty("rows");
231 } catch (Exception e) {
232 }
233
234 if (val == -1) {
235 val = 24;
236 }
237
238 return val;
239 }
240
241 private static int getTerminalProperty(String prop)
242 throws IOException, InterruptedException {
243
244
245
246
247 String props = stty("-a");
248
249 for (StringTokenizer tok = new StringTokenizer(props, ";\n");
250 tok.hasMoreTokens();) {
251 String str = tok.nextToken().trim();
252
253 if (str.startsWith(prop)) {
254 int index = str.lastIndexOf(" ");
255
256 return Integer.parseInt(str.substring(index).trim());
257 } else if (str.endsWith(prop)) {
258 int index = str.indexOf(" ");
259
260 return Integer.parseInt(str.substring(0, index).trim());
261 }
262 }
263
264 return -1;
265 }
266
267 /***
268 * Execute the stty command with the specified arguments
269 * against the current active terminal.
270 */
271 private static String stty(final String args)
272 throws IOException, InterruptedException {
273 return exec("stty " + args + " < /dev/tty").trim();
274 }
275
276 /***
277 * Execute the specified command and return the output
278 * (both stdout and stderr).
279 */
280 private static String exec(final String cmd)
281 throws IOException, InterruptedException {
282 return exec(new String[] {
283 "sh",
284 "-c",
285 cmd
286 });
287 }
288
289 /***
290 * Execute the specified command and return the output
291 * (both stdout and stderr).
292 */
293 private static String exec(final String[] cmd)
294 throws IOException, InterruptedException {
295 ByteArrayOutputStream bout = new ByteArrayOutputStream();
296
297 Process p = Runtime.getRuntime().exec(cmd);
298 int c;
299 InputStream in;
300
301 in = p.getInputStream();
302
303 while ((c = in.read()) != -1) {
304 bout.write(c);
305 }
306
307 in = p.getErrorStream();
308
309 while ((c = in.read()) != -1) {
310 bout.write(c);
311 }
312
313 p.waitFor();
314
315 String result = new String(bout.toByteArray());
316
317 return result;
318 }
319
320 /***
321 * The command to use to set the terminal options. Defaults
322 * to "stty", or the value of the system property "jline.sttyCommand".
323 */
324 public static void setSttyCommand(String cmd) {
325 sttyCommand = cmd;
326 }
327
328 /***
329 * The command to use to set the terminal options. Defaults
330 * to "stty", or the value of the system property "jline.sttyCommand".
331 */
332 public static String getSttyCommand() {
333 return sttyCommand;
334 }
335
336 public synchronized boolean isEchoEnabled() {
337 return echoEnabled;
338 }
339
340
341 public synchronized void enableEcho() {
342 try {
343 stty("echo");
344 echoEnabled = true;
345 } catch (Exception e) {
346 consumeException(e);
347 }
348 }
349
350 public synchronized void disableEcho() {
351 try {
352 stty("-echo");
353 echoEnabled = false;
354 } catch (Exception e) {
355 consumeException(e);
356 }
357 }
358
359 /***
360 * This is awkward and inefficient, but probably the minimal way to add
361 * UTF-8 support to JLine
362 *
363 * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
364 */
365 static class ReplayPrefixOneCharInputStream extends InputStream {
366 byte firstByte;
367 int byteLength;
368 InputStream wrappedStream;
369 int byteRead;
370
371 final String encoding;
372
373 public ReplayPrefixOneCharInputStream(String encoding) {
374 this.encoding = encoding;
375 }
376
377 public void setInput(int recorded, InputStream wrapped) throws IOException {
378 this.byteRead = 0;
379 this.firstByte = (byte) recorded;
380 this.wrappedStream = wrapped;
381
382 byteLength = 1;
383 if (encoding.equalsIgnoreCase("UTF-8"))
384 setInputUTF8(recorded, wrapped);
385 else if (encoding.equalsIgnoreCase("UTF-16"))
386 byteLength = 2;
387 else if (encoding.equalsIgnoreCase("UTF-32"))
388 byteLength = 4;
389 }
390
391
392 public void setInputUTF8(int recorded, InputStream wrapped) throws IOException {
393
394 if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
395 this.byteLength = 2;
396
397 else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
398 this.byteLength = 3;
399
400 else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
401 this.byteLength = 4;
402 else
403 throw new IOException("invalid UTF-8 first byte: " + firstByte);
404 }
405
406 public int read() throws IOException {
407 if (available() == 0)
408 return -1;
409
410 byteRead++;
411
412 if (byteRead == 1)
413 return firstByte;
414
415 return wrappedStream.read();
416 }
417
418 /***
419 * InputStreamReader is greedy and will try to read bytes in advance. We
420 * do NOT want this to happen since we use a temporary/"losing bytes"
421 * InputStreamReader above, that's why we hide the real
422 * wrappedStream.available() here.
423 */
424 public int available() {
425 return byteLength - byteRead;
426 }
427 }
428 }