View Javadoc

1   /*
2    * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
3    *
4    * This software is distributable under the BSD license. See the terms of the
5    * BSD license in the documentation provided with this software.
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          // save the initial tty configuration
77          ttyConfig = stty("-g");
78  
79          // sanity check
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          // set the console to be character-buffered instead of line-buffered
89          stty("-icanon min 1");
90  
91          // disable character echoing
92          stty("-echo");
93          echoEnabled = false;
94  
95          // at exit, restore the original tty configuration (for JDK 1.3+)
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             // 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 		//also the escape key is 27
141 		//thats why we read until we
142 		//have something different than 27
143 		//this is a bugfix, because otherwise
144 		//pressing escape and than an arrow key
145 		//was an undefined state
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); // read 4th
164                     return DELETE;
165                 }
166             } 
167         } 
168         // handle unicode characters, thanks for a patch from amyi@inf.ed.ac.uk
169         if (c > 128) {
170           // handle unicode characters longer than 2 bytes,
171           // thanks to Marc.Herbert@continuent.com
172             replayStream.setInput(c, in);
173 //            replayReader = new InputStreamReader(replayStream, encoding);
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         // need to be able handle both output formats:
244         // speed 9600 baud; 24 rows; 140 columns;
245         // and:
246         // speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0;
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             // 110yyyyy 10zzzzzz
394             if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
395                 this.byteLength = 2;
396             // 1110xxxx 10yyyyyy 10zzzzzz
397             else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
398                 this.byteLength = 3;
399             // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
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 }