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 boolean echoEnabled;
040        private String ttyConfig;
041        private String ttyProps;
042        private long ttyPropsLastFetched;
043        private boolean backspaceDeleteSwitched = false;
044        private static String sttyCommand =
045            System.getProperty("jline.sttyCommand", "stty");
046    
047        
048        String encoding = System.getProperty("input.encoding", "UTF-8");
049        ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding);
050        InputStreamReader replayReader;
051    
052        public UnixTerminal() {
053            try {
054                replayReader = new InputStreamReader(replayStream, encoding);
055            } catch (Exception e) {
056                throw new RuntimeException(e);
057            }
058        }
059       
060        protected void checkBackspace(){
061            String[] ttyConfigSplit = ttyConfig.split(":|=");
062            backspaceDeleteSwitched = ttyConfigSplit.length >= 7 && "7f".equals(ttyConfigSplit[6]);
063        }
064        
065        /**
066         *  Remove line-buffered input by invoking "stty -icanon min 1"
067         *  against the current terminal.
068         */
069        public void initializeTerminal() throws IOException, InterruptedException {
070            // save the initial tty configuration
071            ttyConfig = stty("-g");
072    
073            // sanity check
074            if ((ttyConfig.length() == 0)
075                    || ((ttyConfig.indexOf("=") == -1)
076                           && (ttyConfig.indexOf(":") == -1))) {
077                throw new IOException("Unrecognized stty code: " + ttyConfig);
078            }
079    
080            checkBackspace();
081    
082            // set the console to be character-buffered instead of line-buffered
083            stty("-icanon min 1");
084    
085            // disable character echoing
086            stty("-echo");
087            echoEnabled = false;
088    
089            // at exit, restore the original tty configuration (for JDK 1.3+)
090            try {
091                Runtime.getRuntime().addShutdownHook(new Thread() {
092                        public void start() {
093                            try {
094                                restoreTerminal();
095                            } catch (Exception e) {
096                                consumeException(e);
097                            }
098                        }
099                    });
100            } catch (AbstractMethodError ame) {
101                // JDK 1.3+ only method. Bummer.
102                consumeException(ame);
103            }
104        }
105    
106        /** 
107         * Restore the original terminal configuration, which can be used when
108         * shutting down the console reader. The ConsoleReader cannot be
109         * used after calling this method.
110         */
111        public void restoreTerminal() throws Exception {
112            if (ttyConfig != null) {
113                stty(ttyConfig);
114                ttyConfig = null;
115            }
116            resetTerminal();
117        }
118    
119        
120        
121        public int readVirtualKey(InputStream in) throws IOException {
122            int c = readCharacter(in);
123    
124            if (backspaceDeleteSwitched)
125                if (c == DELETE)
126                    c = BACKSPACE;
127                else if (c == BACKSPACE)
128                    c = DELETE;
129    
130            // in Unix terminals, arrow keys are represented by
131            // a sequence of 3 characters. E.g., the up arrow
132            // key yields 27, 91, 68
133            if (c == ARROW_START && in.available() > 0) {
134                // Escape key is also 27, so we use InputStream.available()
135                // to distinguish those. If 27 represents an arrow, there
136                // should be two more chars immediately available.
137                while (c == ARROW_START) {
138                    c = readCharacter(in);
139                }
140                if (c == ARROW_PREFIX || c == O_PREFIX) {
141                    c = readCharacter(in);
142                    if (c == ARROW_UP) {
143                        return CTRL_P;
144                    } else if (c == ARROW_DOWN) {
145                        return CTRL_N;
146                    } else if (c == ARROW_LEFT) {
147                        return CTRL_B;
148                    } else if (c == ARROW_RIGHT) {
149                        return CTRL_F;
150                    } else if (c == HOME_CODE) {
151                        return CTRL_A;
152                    } else if (c == END_CODE) {
153                        return CTRL_E;
154                    } else if (c == DEL_THIRD) {
155                        c = readCharacter(in); // read 4th
156                        return DELETE;
157                    }
158                } 
159            } 
160            // handle unicode characters, thanks for a patch from amyi@inf.ed.ac.uk
161            if (c > 128) {
162              // handle unicode characters longer than 2 bytes,
163              // thanks to Marc.Herbert@continuent.com
164                replayStream.setInput(c, in);
165    //            replayReader = new InputStreamReader(replayStream, encoding);
166                c = replayReader.read();
167                
168            }
169    
170            return c;
171        }
172    
173        /**
174         *  No-op for exceptions we want to silently consume.
175         */
176        private void consumeException(Throwable e) {
177        }
178    
179        public boolean isSupported() {
180            return true;
181        }
182    
183        public boolean getEcho() {
184            return false;
185        }
186    
187        /**
188         *  Returns the value of "stty size" width param.
189         *
190         *  <strong>Note</strong>: this method caches the value from the
191         *  first time it is called in order to increase speed, which means
192         *  that changing to size of the terminal will not be reflected
193         *  in the console.
194         */
195        public int getTerminalWidth() {
196            int val = -1;
197    
198            try {
199                val = getTerminalProperty("columns");
200            } catch (Exception e) {
201            }
202    
203            if (val == -1) {
204                val = 80;
205            }
206    
207            return val;
208        }
209    
210        /**
211         *  Returns the value of "stty size" height param.
212         *
213         *  <strong>Note</strong>: this method caches the value from the
214         *  first time it is called in order to increase speed, which means
215         *  that changing to size of the terminal will not be reflected
216         *  in the console.
217         */
218        public int getTerminalHeight() {
219            int val = -1;
220    
221            try {
222                val = getTerminalProperty("rows");
223            } catch (Exception e) {
224            }
225    
226            if (val == -1) {
227                val = 24;
228            }
229    
230            return val;
231        }
232    
233        private int getTerminalProperty(String prop)
234                                        throws IOException, InterruptedException {
235            // tty properties are cached so we don't have to worry too much about getting term widht/height
236            if (ttyProps == null || System.currentTimeMillis() - ttyPropsLastFetched > 1000) {
237                ttyProps = stty("-a");
238                ttyPropsLastFetched = System.currentTimeMillis();
239            }
240            // need to be able handle both output formats:
241            // speed 9600 baud; 24 rows; 140 columns;
242            // and:
243            // speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0;
244            for (StringTokenizer tok = new StringTokenizer(ttyProps, ";\n");
245                     tok.hasMoreTokens();) {
246                String str = tok.nextToken().trim();
247    
248                if (str.startsWith(prop)) {
249                    int index = str.lastIndexOf(" ");
250    
251                    return Integer.parseInt(str.substring(index).trim());
252                } else if (str.endsWith(prop)) {
253                    int index = str.indexOf(" ");
254    
255                    return Integer.parseInt(str.substring(0, index).trim());
256                }
257            }
258    
259            return -1;
260        }
261    
262        /**
263         *  Execute the stty command with the specified arguments
264         *  against the current active terminal.
265         */
266        protected static String stty(final String args)
267                            throws IOException, InterruptedException {
268            return exec("stty " + args + " < /dev/tty").trim();
269        }
270    
271        /**
272         *  Execute the specified command and return the output
273         *  (both stdout and stderr).
274         */
275        private static String exec(final String cmd)
276                            throws IOException, InterruptedException {
277            return exec(new String[] {
278                            "sh",
279                            "-c",
280                            cmd
281                        });
282        }
283    
284        /**
285         *  Execute the specified command and return the output
286         *  (both stdout and stderr).
287         */
288        private static String exec(final String[] cmd)
289                            throws IOException, InterruptedException {
290            ByteArrayOutputStream bout = new ByteArrayOutputStream();
291    
292            Process p = Runtime.getRuntime().exec(cmd);
293            int c;
294            InputStream in = null;
295            InputStream err = null;
296            OutputStream out = null;
297    
298            try {
299                    in = p.getInputStream();
300    
301                    while ((c = in.read()) != -1) {
302                        bout.write(c);
303                    }
304    
305                    err = p.getErrorStream();
306    
307                    while ((c = err.read()) != -1) {
308                        bout.write(c);
309                    }
310            
311                    out = p.getOutputStream();
312    
313                    p.waitFor();
314                } finally {
315                        try {in.close();} catch (Exception e) {}
316                        try {err.close();} catch (Exception e) {}
317                        try {out.close();} catch (Exception e) {}
318                }
319    
320            String result = new String(bout.toByteArray());
321    
322            return result;
323        }
324    
325        /**
326         *  The command to use to set the terminal options. Defaults
327         *  to "stty", or the value of the system property "jline.sttyCommand".
328         */
329        public static void setSttyCommand(String cmd) {
330            sttyCommand = cmd;
331        }
332    
333        /**
334         *  The command to use to set the terminal options. Defaults
335         *  to "stty", or the value of the system property "jline.sttyCommand".
336         */
337        public static String getSttyCommand() {
338            return sttyCommand;
339        }
340    
341        public synchronized boolean isEchoEnabled() {
342            return echoEnabled;
343        }
344    
345    
346        public synchronized void enableEcho() {
347            try {
348                            stty("echo");
349                echoEnabled = true;
350                    } catch (Exception e) {
351                            consumeException(e);
352                    }
353        }
354    
355        public synchronized void disableEcho() {
356            try {
357                            stty("-echo");
358                echoEnabled = false;
359                    } catch (Exception e) {
360                            consumeException(e);
361                    }
362        }
363    
364        /**
365         * This is awkward and inefficient, but probably the minimal way to add
366         * UTF-8 support to JLine
367         *
368         * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
369         */
370        static class ReplayPrefixOneCharInputStream extends InputStream {
371            byte firstByte;
372            int byteLength;
373            InputStream wrappedStream;
374            int byteRead;
375    
376            final String encoding;
377            
378            public ReplayPrefixOneCharInputStream(String encoding) {
379                this.encoding = encoding;
380            }
381            
382            public void setInput(int recorded, InputStream wrapped) throws IOException {
383                this.byteRead = 0;
384                this.firstByte = (byte) recorded;
385                this.wrappedStream = wrapped;
386    
387                byteLength = 1;
388                if (encoding.equalsIgnoreCase("UTF-8"))
389                    setInputUTF8(recorded, wrapped);
390                else if (encoding.equalsIgnoreCase("UTF-16"))
391                    byteLength = 2;
392                else if (encoding.equalsIgnoreCase("UTF-32"))
393                    byteLength = 4;
394            }
395                
396                
397            public void setInputUTF8(int recorded, InputStream wrapped) throws IOException {
398                // 110yyyyy 10zzzzzz
399                if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
400                    this.byteLength = 2;
401                // 1110xxxx 10yyyyyy 10zzzzzz
402                else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
403                    this.byteLength = 3;
404                // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
405                else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
406                    this.byteLength = 4;
407                else
408                    throw new IOException("invalid UTF-8 first byte: " + firstByte);
409            }
410    
411            public int read() throws IOException {
412                if (available() == 0)
413                    return -1;
414    
415                byteRead++;
416    
417                if (byteRead == 1)
418                    return firstByte;
419    
420                return wrappedStream.read();
421            }
422    
423            /**
424            * InputStreamReader is greedy and will try to read bytes in advance. We
425            * do NOT want this to happen since we use a temporary/"losing bytes"
426            * InputStreamReader above, that's why we hide the real
427            * wrappedStream.available() here.
428            */
429            public int available() {
430                return byteLength - byteRead;
431            }
432        }
433    }