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 */
007package jline;
008
009import java.io.*;
010import 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 */
025public 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}