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                    //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    }