001    /*
002     * Copyright (c) 2002-2006, 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 HOME_CODE = 72;
033        public static final short END_CODE = 70;
034        private Map terminfo;
035        private static String sttyCommand =
036            System.getProperty("jline.sttyCommand", "stty");
037    
038        /**
039         *  Remove line-buffered input by invoking "stty -icanon min 1"
040         *  against the current terminal.
041         */
042        public void initializeTerminal() throws IOException, InterruptedException {
043            // save the initial tty configuration
044            final String ttyConfig = stty("-g");
045    
046            // sanity check
047            if ((ttyConfig.length() == 0)
048                    || ((ttyConfig.indexOf("=") == -1)
049                           && (ttyConfig.indexOf(":") == -1))) {
050                throw new IOException("Unrecognized stty code: " + ttyConfig);
051            }
052    
053            // set the console to be character-buffered instead of line-buffered
054            stty("-icanon min 1");
055    
056            // disable character echoing
057            stty("-echo");
058    
059            // at exit, restore the original tty configuration (for JDK 1.3+)
060            try {
061                Runtime.getRuntime().addShutdownHook(new Thread() {
062                        public void start() {
063                            try {
064                                stty(ttyConfig);
065                            } catch (Exception e) {
066                                consumeException(e);
067                            }
068                        }
069                    });
070            } catch (AbstractMethodError ame) {
071                // JDK 1.3+ only method. Bummer.
072                consumeException(ame);
073            }
074        }
075    
076        public int readVirtualKey(InputStream in) throws IOException {
077            int c = readCharacter(in);
078    
079            // in Unix terminals, arrow keys are represented by
080            // a sequence of 3 characters. E.g., the up arrow
081            // key yields 27, 91, 68
082            if (c == ARROW_START) {
083                c = readCharacter(in);
084    
085                if (c == ARROW_PREFIX) {
086                    c = readCharacter(in);
087    
088                    if (c == ARROW_UP) {
089                        return CTRL_P;
090                    } else if (c == ARROW_DOWN) {
091                        return CTRL_N;
092                    } else if (c == ARROW_LEFT) {
093                        return CTRL_B;
094                    } else if (c == ARROW_RIGHT) {
095                        return CTRL_F;
096                    } else if (c == HOME_CODE) {
097                        return CTRL_A;
098                    } else if (c == END_CODE) {
099                        return CTRL_E;
100                    }
101                }
102            }
103    
104            return c;
105        }
106    
107        /**
108         *  No-op for exceptions we want to silently consume.
109         */
110        private void consumeException(Throwable e) {
111        }
112    
113        public boolean isSupported() {
114            return true;
115        }
116    
117        public boolean getEcho() {
118            return false;
119        }
120    
121        /**
122         *  Returns the value of "stty size" width param.
123         *
124         *  <strong>Note</strong>: this method caches the value from the
125         *  first time it is called in order to increase speed, which means
126         *  that changing to size of the terminal will not be reflected
127         *  in the console.
128         */
129        public int getTerminalWidth() {
130            int val = -1;
131    
132            try {
133                val = getTerminalProperty("columns");
134            } catch (Exception e) {
135            }
136    
137            if (val == -1) {
138                val = 80;
139            }
140    
141            return val;
142        }
143    
144        /**
145         *  Returns the value of "stty size" height param.
146         *
147         *  <strong>Note</strong>: this method caches the value from the
148         *  first time it is called in order to increase speed, which means
149         *  that changing to size of the terminal will not be reflected
150         *  in the console.
151         */
152        public int getTerminalHeight() {
153            int val = -1;
154    
155            try {
156                val = getTerminalProperty("rows");
157            } catch (Exception e) {
158            }
159    
160            if (val == -1) {
161                val = 24;
162            }
163    
164            return val;
165        }
166    
167        private static int getTerminalProperty(String prop)
168                                        throws IOException, InterruptedException {
169            // need to be able handle both output formats:
170            // speed 9600 baud; 24 rows; 140 columns;
171            // and:
172            // speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0;
173            String props = stty("-a");
174    
175            for (StringTokenizer tok = new StringTokenizer(props, ";\n");
176                     tok.hasMoreTokens();) {
177                String str = tok.nextToken().trim();
178    
179                if (str.startsWith(prop)) {
180                    int index = str.lastIndexOf(" ");
181    
182                    return Integer.parseInt(str.substring(index).trim());
183                } else if (str.endsWith(prop)) {
184                    int index = str.indexOf(" ");
185    
186                    return Integer.parseInt(str.substring(0, index).trim());
187                }
188            }
189    
190            return -1;
191        }
192    
193        /**
194         *  Execute the stty command with the specified arguments
195         *  against the current active terminal.
196         */
197        private static String stty(final String args)
198                            throws IOException, InterruptedException {
199            return exec("stty " + args + " < /dev/tty").trim();
200        }
201    
202        /**
203         *  Execute the specified command and return the output
204         *  (both stdout and stderr).
205         */
206        private static String exec(final String cmd)
207                            throws IOException, InterruptedException {
208            return exec(new String[] {
209                            "sh",
210                            "-c",
211                            cmd
212                        });
213        }
214    
215        /**
216         *  Execute the specified command and return the output
217         *  (both stdout and stderr).
218         */
219        private static String exec(final String[] cmd)
220                            throws IOException, InterruptedException {
221            ByteArrayOutputStream bout = new ByteArrayOutputStream();
222    
223            Process p = Runtime.getRuntime().exec(cmd);
224            int c;
225            InputStream in;
226    
227            in = p.getInputStream();
228    
229            while ((c = in.read()) != -1) {
230                bout.write(c);
231            }
232    
233            in = p.getErrorStream();
234    
235            while ((c = in.read()) != -1) {
236                bout.write(c);
237            }
238    
239            p.waitFor();
240    
241            String result = new String(bout.toByteArray());
242    
243            return result;
244        }
245    
246        /**
247         *  The command to use to set the terminal options. Defaults
248         *  to "stty", or the value of the system property "jline.sttyCommand".
249         */
250        public static void setSttyCommand(String cmd) {
251            sttyCommand = cmd;
252        }
253    
254        /**
255         *  The command to use to set the terminal options. Defaults
256         *  to "stty", or the value of the system property "jline.sttyCommand".
257         */
258        public static String getSttyCommand() {
259            return sttyCommand;
260        }
261    
262        public static void main(String[] args) {
263            System.out.println("width: " + new UnixTerminal().getTerminalWidth());
264            System.out.println("height: " + new UnixTerminal().getTerminalHeight());
265        }
266    }