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    }