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