001 /*
002 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
003 *
004 * This software is distributable under the BSD license. See the terms of the
005 * BSD license in the documentation provided with this software.
006 */
007 package jline;
008
009 import java.io.*;
010 import java.util.*;
011
012 /**
013 * <p>
014 * Terminal that is used for unix platforms. Terminal initialization
015 * is handled by issuing the <em>stty</em> command against the
016 * <em>/dev/tty</em> file to disable character echoing and enable
017 * character input. All known unix systems (including
018 * Linux and Macintosh OS X) support the <em>stty</em>), so this
019 * implementation should work for an reasonable POSIX system.
020 * </p>
021 *
022 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
023 * @author Updates <a href="mailto:dwkemp@gmail.com">Dale Kemp</a> 2005-12-03
024 */
025 public class UnixTerminal extends Terminal {
026 public static final short ARROW_START = 27;
027 public static final short ARROW_PREFIX = 91;
028 public static final short ARROW_LEFT = 68;
029 public static final short ARROW_RIGHT = 67;
030 public static final short ARROW_UP = 65;
031 public static final short ARROW_DOWN = 66;
032 public static final short O_PREFIX = 79;
033 public static final short HOME_CODE = 72;
034 public static final short END_CODE = 70;
035
036 public static final short DEL_THIRD = 51;
037 public static final short DEL_SECOND = 126;
038
039 private Map terminfo;
040 private boolean echoEnabled;
041 private String ttyConfig;
042 private boolean backspaceDeleteSwitched = false;
043 private static String sttyCommand =
044 System.getProperty("jline.sttyCommand", "stty");
045
046
047 String encoding = System.getProperty("input.encoding", "UTF-8");
048 ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding);
049 InputStreamReader replayReader;
050
051 public UnixTerminal() {
052 try {
053 replayReader = new InputStreamReader(replayStream, encoding);
054 } catch (Exception e) {
055 throw new RuntimeException(e);
056 }
057 }
058
059 protected void checkBackspace(){
060 String[] ttyConfigSplit = ttyConfig.split(":|=");
061
062 if (ttyConfigSplit.length < 7)
063 return;
064
065 if (ttyConfigSplit[6] == null)
066 return;
067
068 backspaceDeleteSwitched = ttyConfigSplit[6].equals("7f");
069 }
070
071 /**
072 * Remove line-buffered input by invoking "stty -icanon min 1"
073 * against the current terminal.
074 */
075 public void initializeTerminal() throws IOException, InterruptedException {
076 // save the initial tty configuration
077 ttyConfig = stty("-g");
078
079 // sanity check
080 if ((ttyConfig.length() == 0)
081 || ((ttyConfig.indexOf("=") == -1)
082 && (ttyConfig.indexOf(":") == -1))) {
083 throw new IOException("Unrecognized stty code: " + ttyConfig);
084 }
085
086 checkBackspace();
087
088 // set the console to be character-buffered instead of line-buffered
089 stty("-icanon min 1");
090
091 // disable character echoing
092 stty("-echo");
093 echoEnabled = false;
094
095 // at exit, restore the original tty configuration (for JDK 1.3+)
096 try {
097 Runtime.getRuntime().addShutdownHook(new Thread() {
098 public void start() {
099 try {
100 restoreTerminal();
101 } catch (Exception e) {
102 consumeException(e);
103 }
104 }
105 });
106 } catch (AbstractMethodError ame) {
107 // JDK 1.3+ only method. Bummer.
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 // in Unix terminals, arrow keys are represented by
137 // a sequence of 3 characters. E.g., the up arrow
138 // key yields 27, 91, 68
139 if (c == ARROW_START) {
140 c = readCharacter(in);
141 if (c == ARROW_PREFIX || c == O_PREFIX) {
142 c = readCharacter(in);
143 if (c == ARROW_UP) {
144 return CTRL_P;
145 } else if (c == ARROW_DOWN) {
146 return CTRL_N;
147 } else if (c == ARROW_LEFT) {
148 return CTRL_B;
149 } else if (c == ARROW_RIGHT) {
150 return CTRL_F;
151 } else if (c == HOME_CODE) {
152 return CTRL_A;
153 } else if (c == END_CODE) {
154 return CTRL_E;
155 } else if (c == DEL_THIRD) {
156 c = readCharacter(in); // read 4th
157 return DELETE;
158 }
159 }
160 }
161 // handle unicode characters, thanks for a patch from amyi@inf.ed.ac.uk
162 if (c > 128) {
163 // handle unicode characters longer than 2 bytes,
164 // thanks to Marc.Herbert@continuent.com
165 replayStream.setInput(c, in);
166 // replayReader = new InputStreamReader(replayStream, encoding);
167 c = replayReader.read();
168
169 }
170
171 return c;
172 }
173
174 /**
175 * No-op for exceptions we want to silently consume.
176 */
177 private void consumeException(Throwable e) {
178 }
179
180 public boolean isSupported() {
181 return true;
182 }
183
184 public boolean getEcho() {
185 return false;
186 }
187
188 /**
189 * Returns the value of "stty size" width param.
190 *
191 * <strong>Note</strong>: this method caches the value from the
192 * first time it is called in order to increase speed, which means
193 * that changing to size of the terminal will not be reflected
194 * in the console.
195 */
196 public int getTerminalWidth() {
197 int val = -1;
198
199 try {
200 val = getTerminalProperty("columns");
201 } catch (Exception e) {
202 }
203
204 if (val == -1) {
205 val = 80;
206 }
207
208 return val;
209 }
210
211 /**
212 * Returns the value of "stty size" height param.
213 *
214 * <strong>Note</strong>: this method caches the value from the
215 * first time it is called in order to increase speed, which means
216 * that changing to size of the terminal will not be reflected
217 * in the console.
218 */
219 public int getTerminalHeight() {
220 int val = -1;
221
222 try {
223 val = getTerminalProperty("rows");
224 } catch (Exception e) {
225 }
226
227 if (val == -1) {
228 val = 24;
229 }
230
231 return val;
232 }
233
234 private static int getTerminalProperty(String prop)
235 throws IOException, InterruptedException {
236 // need to be able handle both output formats:
237 // speed 9600 baud; 24 rows; 140 columns;
238 // and:
239 // speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0;
240 String props = stty("-a");
241
242 for (StringTokenizer tok = new StringTokenizer(props, ";\n");
243 tok.hasMoreTokens();) {
244 String str = tok.nextToken().trim();
245
246 if (str.startsWith(prop)) {
247 int index = str.lastIndexOf(" ");
248
249 return Integer.parseInt(str.substring(index).trim());
250 } else if (str.endsWith(prop)) {
251 int index = str.indexOf(" ");
252
253 return Integer.parseInt(str.substring(0, index).trim());
254 }
255 }
256
257 return -1;
258 }
259
260 /**
261 * Execute the stty command with the specified arguments
262 * against the current active terminal.
263 */
264 private static String stty(final String args)
265 throws IOException, InterruptedException {
266 return exec("stty " + args + " < /dev/tty").trim();
267 }
268
269 /**
270 * Execute the specified command and return the output
271 * (both stdout and stderr).
272 */
273 private static String exec(final String cmd)
274 throws IOException, InterruptedException {
275 return exec(new String[] {
276 "sh",
277 "-c",
278 cmd
279 });
280 }
281
282 /**
283 * Execute the specified command and return the output
284 * (both stdout and stderr).
285 */
286 private static String exec(final String[] cmd)
287 throws IOException, InterruptedException {
288 ByteArrayOutputStream bout = new ByteArrayOutputStream();
289
290 Process p = Runtime.getRuntime().exec(cmd);
291 int c;
292 InputStream in;
293
294 in = p.getInputStream();
295
296 while ((c = in.read()) != -1) {
297 bout.write(c);
298 }
299
300 in = p.getErrorStream();
301
302 while ((c = in.read()) != -1) {
303 bout.write(c);
304 }
305
306 p.waitFor();
307
308 String result = new String(bout.toByteArray());
309
310 return result;
311 }
312
313 /**
314 * The command to use to set the terminal options. Defaults
315 * to "stty", or the value of the system property "jline.sttyCommand".
316 */
317 public static void setSttyCommand(String cmd) {
318 sttyCommand = cmd;
319 }
320
321 /**
322 * The command to use to set the terminal options. Defaults
323 * to "stty", or the value of the system property "jline.sttyCommand".
324 */
325 public static String getSttyCommand() {
326 return sttyCommand;
327 }
328
329 public synchronized boolean isEchoEnabled() {
330 return echoEnabled;
331 }
332
333
334 public synchronized void enableEcho() {
335 try {
336 stty("echo");
337 echoEnabled = true;
338 } catch (Exception e) {
339 consumeException(e);
340 }
341 }
342
343 public synchronized void disableEcho() {
344 try {
345 stty("-echo");
346 echoEnabled = false;
347 } catch (Exception e) {
348 consumeException(e);
349 }
350 }
351
352 /**
353 * This is awkward and inefficient, but probably the minimal way to add
354 * UTF-8 support to JLine
355 *
356 * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
357 */
358 static class ReplayPrefixOneCharInputStream extends InputStream {
359 byte firstByte;
360 int byteLength;
361 InputStream wrappedStream;
362 int byteRead;
363
364 final String encoding;
365
366 public ReplayPrefixOneCharInputStream(String encoding) {
367 this.encoding = encoding;
368 }
369
370 public void setInput(int recorded, InputStream wrapped) throws IOException {
371 this.byteRead = 0;
372 this.firstByte = (byte) recorded;
373 this.wrappedStream = wrapped;
374
375 byteLength = 1;
376 if (encoding.equalsIgnoreCase("UTF-8"))
377 setInputUTF8(recorded, wrapped);
378 else if (encoding.equalsIgnoreCase("UTF-16"))
379 byteLength = 2;
380 else if (encoding.equalsIgnoreCase("UTF-32"))
381 byteLength = 4;
382 }
383
384
385 public void setInputUTF8(int recorded, InputStream wrapped) throws IOException {
386 // 110yyyyy 10zzzzzz
387 if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
388 this.byteLength = 2;
389 // 1110xxxx 10yyyyyy 10zzzzzz
390 else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
391 this.byteLength = 3;
392 // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
393 else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
394 this.byteLength = 4;
395 else
396 throw new IOException("invalid UTF-8 first byte: " + firstByte);
397 }
398
399 public int read() throws IOException {
400 if (available() == 0)
401 return -1;
402
403 byteRead++;
404
405 if (byteRead == 1)
406 return firstByte;
407
408 return wrappedStream.read();
409 }
410
411 /**
412 * InputStreamReader is greedy and will try to read bytes in advance. We
413 * do NOT want this to happen since we use a temporary/"losing bytes"
414 * InputStreamReader above, that's why we hide the real
415 * wrappedStream.available() here.
416 */
417 public int available() {
418 return byteLength - byteRead;
419 }
420 }
421 }