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    
011    import jline.UnixTerminal.ReplayPrefixOneCharInputStream;
012    
013    /**
014     * <p>
015     * Terminal implementation for Microsoft Windows. Terminal initialization in
016     * {@link #initializeTerminal} is accomplished by extracting the
017     * <em>jline_<i>version</i>.dll</em>, saving it to the system temporary
018     * directoy (determined by the setting of the <em>java.io.tmpdir</em> System
019     * property), loading the library, and then calling the Win32 APIs <a
020     * href="http://msdn.microsoft.com/library/default.asp?
021     * url=/library/en-us/dllproc/base/setconsolemode.asp">SetConsoleMode</a> and
022     * <a href="http://msdn.microsoft.com/library/default.asp?
023     * url=/library/en-us/dllproc/base/getconsolemode.asp">GetConsoleMode</a> to
024     * disable character echoing.
025     * </p>
026     *
027     * <p>
028     * By default, the {@link #readCharacter} method will attempt to test to see if
029     * the specified {@link InputStream} is {@link System#in} or a wrapper around
030     * {@link FileDescriptor#in}, and if so, will bypass the character reading to
031     * directly invoke the readc() method in the JNI library. This is so the class
032     * can read special keys (like arrow keys) which are otherwise inaccessible via
033     * the {@link System#in} stream. Using JNI reading can be bypassed by setting
034     * the <code>jline.WindowsTerminal.directConsole</code> system property
035     * to <code>false</code>.
036     * </p>
037     *
038     * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
039     */
040    public class WindowsTerminal extends Terminal {
041        // constants copied from wincon.h
042    
043        /**
044         * The ReadFile or ReadConsole function returns only when a carriage return
045         * character is read. If this mode is disable, the functions return when one
046         * or more characters are available.
047         */
048        private static final int ENABLE_LINE_INPUT = 2;
049    
050        /**
051         * Characters read by the ReadFile or ReadConsole function are written to
052         * the active screen buffer as they are read. This mode can be used only if
053         * the ENABLE_LINE_INPUT mode is also enabled.
054         */
055        private static final int ENABLE_ECHO_INPUT = 4;
056    
057        /**
058         * CTRL+C is processed by the system and is not placed in the input buffer.
059         * If the input buffer is being read by ReadFile or ReadConsole, other
060         * control keys are processed by the system and are not returned in the
061         * ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also
062         * enabled, backspace, carriage return, and linefeed characters are handled
063         * by the system.
064         */
065        private static final int ENABLE_PROCESSED_INPUT = 1;
066    
067        /**
068         * User interactions that change the size of the console screen buffer are
069         * reported in the console's input buffee. Information about these events
070         * can be read from the input buffer by applications using
071         * theReadConsoleInput function, but not by those using ReadFile
072         * orReadConsole.
073         */
074        private static final int ENABLE_WINDOW_INPUT = 8;
075    
076        /**
077         * If the mouse pointer is within the borders of the console window and the
078         * window has the keyboard focus, mouse events generated by mouse movement
079         * and button presses are placed in the input buffer. These events are
080         * discarded by ReadFile or ReadConsole, even when this mode is enabled.
081         */
082        private static final int ENABLE_MOUSE_INPUT = 16;
083    
084        /**
085         * When enabled, text entered in a console window will be inserted at the
086         * current cursor location and all text following that location will not be
087         * overwritten. When disabled, all following text will be overwritten. An OR
088         * operation must be performed with this flag and the ENABLE_EXTENDED_FLAGS
089         * flag to enable this functionality.
090         */
091        private static final int ENABLE_PROCESSED_OUTPUT = 1;
092    
093        /**
094         * This flag enables the user to use the mouse to select and edit text. To
095         * enable this option, use the OR to combine this flag with
096         * ENABLE_EXTENDED_FLAGS.
097         */
098        private static final int ENABLE_WRAP_AT_EOL_OUTPUT = 2;
099    
100        /**
101         * On windows terminals, this character indicates that a 'special' key has
102         * been pressed. This means that a key such as an arrow key, or delete, or
103         * home, etc. will be indicated by the next character.
104         */
105        public static final int SPECIAL_KEY_INDICATOR = 224;
106    
107        /**
108         * On windows terminals, this character indicates that a special key on the
109         * number pad has been pressed.
110         */
111        public static final int NUMPAD_KEY_INDICATOR = 0;
112    
113        /**
114         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR,
115         * this character indicates an left arrow key press.
116         */
117        public static final int LEFT_ARROW_KEY = 75;
118    
119        /**
120         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
121         * this character indicates an
122         * right arrow key press.
123         */
124        public static final int RIGHT_ARROW_KEY = 77;
125    
126        /**
127         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
128         * this character indicates an up
129         * arrow key press.
130         */
131        public static final int UP_ARROW_KEY = 72;
132    
133        /**
134         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
135         * this character indicates an
136         * down arrow key press.
137         */
138        public static final int DOWN_ARROW_KEY = 80;
139    
140        /**
141         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
142         * this character indicates that
143         * the delete key was pressed.
144         */
145        public static final int DELETE_KEY = 83;
146    
147        /**
148         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
149         * this character indicates that
150         * the home key was pressed.
151         */
152        public static final int HOME_KEY = 71;
153    
154        /**
155         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
156         * this character indicates that
157         * the end key was pressed.
158         */
159        public static final char END_KEY = 79;
160    
161        /**
162         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
163         * this character indicates that
164         * the page up key was pressed.
165         */
166        public static final char PAGE_UP_KEY = 73;
167    
168        /**
169         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
170         * this character indicates that
171         * the page down key was pressed.
172         */
173        public static final char PAGE_DOWN_KEY = 81;
174    
175        /**
176         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
177         * this character indicates that
178         * the insert key was pressed.
179         */
180        public static final char INSERT_KEY = 82;
181    
182        /**
183         * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR,
184         * this character indicates that the escape key was pressed.
185         */
186        public static final char ESCAPE_KEY = 0;
187    
188        private Boolean directConsole;
189    
190        private boolean echoEnabled;
191        
192        String encoding = System.getProperty("jline.WindowsTerminal.input.encoding", System.getProperty("file.encoding"));
193        ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding);
194        InputStreamReader replayReader;
195        
196        public WindowsTerminal() {
197            String dir = System.getProperty("jline.WindowsTerminal.directConsole");
198    
199            if ("true".equals(dir)) {
200                directConsole = Boolean.TRUE;
201            } else if ("false".equals(dir)) {
202                directConsole = Boolean.FALSE;
203            }
204            
205            try {
206                replayReader = new InputStreamReader(replayStream, encoding);
207            } catch (Exception e) {
208                throw new RuntimeException(e);
209            }
210            
211        }
212    
213        private native int getConsoleMode();
214    
215        private native void setConsoleMode(final int mode);
216    
217        private native int readByte();
218    
219        private native int getWindowsTerminalWidth();
220    
221        private native int getWindowsTerminalHeight();
222    
223        public int readCharacter(final InputStream in) throws IOException {
224            // if we can detect that we are directly wrapping the system
225            // input, then bypass the input stream and read directly (which
226            // allows us to access otherwise unreadable strokes, such as
227            // the arrow keys)
228            if (directConsole == Boolean.FALSE) {
229                return super.readCharacter(in);
230            } else if ((directConsole == Boolean.TRUE)
231                || ((in == System.in) || (in instanceof FileInputStream
232                    && (((FileInputStream) in).getFD() == FileDescriptor.in)))) {
233                return readByte();
234            } else {
235                return super.readCharacter(in);
236            }
237        }
238    
239        public void initializeTerminal() throws Exception {
240            loadLibrary("jline");
241    
242            final int originalMode = getConsoleMode();
243    
244            setConsoleMode(originalMode & ~ENABLE_ECHO_INPUT);
245    
246            // set the console to raw mode
247            int newMode = originalMode
248                & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT
249                    | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT);
250            echoEnabled = false;
251            setConsoleMode(newMode);
252    
253            // at exit, restore the original tty configuration (for JDK 1.3+)
254            try {
255                Runtime.getRuntime().addShutdownHook(new Thread() {
256                    public void start() {
257                        // restore the old console mode
258                        setConsoleMode(originalMode);
259                    }
260                });
261            } catch (AbstractMethodError ame) {
262                // JDK 1.3+ only method. Bummer.
263                consumeException(ame);
264            }
265        }
266    
267        private void loadLibrary(final String name) throws IOException {
268            // store the DLL in the temporary directory for the System
269            String version = WindowsTerminal.class.getPackage().getImplementationVersion();
270    
271            if (version == null) {
272                version = "";
273            }
274    
275            version = version.replace('.', '_');
276    
277            File f = new File(System.getProperty("java.io.tmpdir"), name + "_"
278                    + version + ".dll");
279            boolean exists = f.isFile(); // check if it already exists
280    
281            // extract the embedded jline.dll file from the jar and save
282            // it to the current directory
283            int bits = 32;
284    
285            // check for 64-bit systems and use to appropriate DLL
286            if (System.getProperty("os.arch").indexOf("64") != -1)
287                bits = 64;
288    
289            InputStream in = new BufferedInputStream(WindowsTerminal.class.getResourceAsStream(name + bits + ".dll"));
290    
291            OutputStream fout = null;
292            try {
293                fout = new BufferedOutputStream(
294                        new FileOutputStream(f));
295                byte[] bytes = new byte[1024 * 10];
296    
297                for (int n = 0; n != -1; n = in.read(bytes)) {
298                    fout.write(bytes, 0, n);
299                }
300    
301            } catch (IOException ioe) {
302                // We might get an IOException trying to overwrite an existing
303                // jline.dll file if there is another process using the DLL.
304                // If this happens, ignore errors.
305                if (!exists) {
306                    throw ioe;
307                }
308            } finally {
309                    if (fout != null) {
310                            try {
311                                    fout.close();
312                            } catch (IOException ioe) {
313                                    // ignore
314                            }
315                    }
316            }
317    
318            // try to clean up the DLL after the JVM exits
319            f.deleteOnExit();
320    
321            // now actually load the DLL
322            System.load(f.getAbsolutePath());
323        }
324    
325        public int readVirtualKey(InputStream in) throws IOException {
326            int indicator = readCharacter(in);
327    
328            // in Windows terminals, arrow keys are represented by
329            // a sequence of 2 characters. E.g., the up arrow
330            // key yields 224, 72
331            if (indicator == SPECIAL_KEY_INDICATOR
332                    || indicator == NUMPAD_KEY_INDICATOR) {
333                int key = readCharacter(in);
334    
335                switch (key) {
336                case UP_ARROW_KEY:
337                    return CTRL_P; // translate UP -> CTRL-P
338                case LEFT_ARROW_KEY:
339                    return CTRL_B; // translate LEFT -> CTRL-B
340                case RIGHT_ARROW_KEY:
341                    return CTRL_F; // translate RIGHT -> CTRL-F
342                case DOWN_ARROW_KEY:
343                    return CTRL_N; // translate DOWN -> CTRL-N
344                case DELETE_KEY:
345                    return CTRL_QM; // translate DELETE -> CTRL-?
346                case HOME_KEY:
347                    return CTRL_A;
348                case END_KEY:
349                    return CTRL_E;
350                case PAGE_UP_KEY:
351                    return CTRL_K;
352                case PAGE_DOWN_KEY:
353                    return CTRL_L;
354                case ESCAPE_KEY:
355                    return CTRL_OB; // translate ESCAPE -> CTRL-[
356                case INSERT_KEY:
357                    return CTRL_C;
358                default:
359                    return 0;
360                }
361            } else if (indicator > 128) {
362                    // handle unicode characters longer than 2 bytes,
363                    // thanks to Marc.Herbert@continuent.com
364                    replayStream.setInput(indicator, in);
365                    // replayReader = new InputStreamReader(replayStream, encoding);
366                    indicator = replayReader.read();
367                    
368            }
369            
370            return indicator;
371            
372            }
373    
374        public boolean isSupported() {
375            return true;
376        }
377    
378        /**
379         * Windows doesn't support ANSI codes by default; disable them.
380         */
381        public boolean isANSISupported() {
382            return false;
383        }
384    
385        public boolean getEcho() {
386            return false;
387        }
388    
389        /**
390         * Unsupported; return the default.
391         *
392         * @see Terminal#getTerminalWidth
393         */
394        public int getTerminalWidth() {
395            return getWindowsTerminalWidth();
396        }
397    
398        /**
399         * Unsupported; return the default.
400         *
401         * @see Terminal#getTerminalHeight
402         */
403        public int getTerminalHeight() {
404            return getWindowsTerminalHeight();
405        }
406    
407        /**
408         * No-op for exceptions we want to silently consume.
409         */
410        private void consumeException(final Throwable e) {
411        }
412    
413        /**
414         * Whether or not to allow the use of the JNI console interaction.
415         */
416        public void setDirectConsole(Boolean directConsole) {
417            this.directConsole = directConsole;
418        }
419    
420        /**
421         * Whether or not to allow the use of the JNI console interaction.
422         */
423        public Boolean getDirectConsole() {
424            return this.directConsole;
425        }
426    
427        public synchronized boolean isEchoEnabled() {
428            return echoEnabled;
429        }
430    
431        public synchronized void enableEcho() {
432            // Must set these four modes at the same time to make it work fine.
433            setConsoleMode(getConsoleMode() | ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT
434                | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT);
435            echoEnabled = true;
436        }
437    
438        public synchronized void disableEcho() {
439            // Must set these four modes at the same time to make it work fine.
440            setConsoleMode(getConsoleMode()
441                & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT
442                    | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT));
443            echoEnabled = true;
444        }
445    
446        public InputStream getDefaultBindings() {
447            return WindowsTerminal.class.getResourceAsStream("windowsbindings.properties");
448        }
449        
450        /**
451         * This is awkward and inefficient, but probably the minimal way to add
452         * UTF-8 support to JLine
453         *
454         * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
455         */
456        static class ReplayPrefixOneCharInputStream extends InputStream {
457            byte firstByte;
458            int byteLength;
459            InputStream wrappedStream;
460            int byteRead;
461    
462            final String encoding;
463            
464            public ReplayPrefixOneCharInputStream(String encoding) {
465                this.encoding = encoding;
466            }
467            
468            public void setInput(int recorded, InputStream wrapped) throws IOException {
469                this.byteRead = 0;
470                this.firstByte = (byte) recorded;
471                this.wrappedStream = wrapped;
472    
473                byteLength = 1;
474                if (encoding.equalsIgnoreCase("UTF-8"))
475                    setInputUTF8(recorded, wrapped);
476                else if (encoding.equalsIgnoreCase("UTF-16"))
477                    byteLength = 2;
478                else if (encoding.equalsIgnoreCase("UTF-32"))
479                    byteLength = 4;
480            }
481                
482                
483            public void setInputUTF8(int recorded, InputStream wrapped) throws IOException {
484                // 110yyyyy 10zzzzzz
485                if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
486                    this.byteLength = 2;
487                // 1110xxxx 10yyyyyy 10zzzzzz
488                else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
489                    this.byteLength = 3;
490                // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
491                else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
492                    this.byteLength = 4;
493                else
494                    throw new IOException("invalid UTF-8 first byte: " + firstByte);
495            }
496    
497            public int read() throws IOException {
498                if (available() == 0)
499                    return -1;
500    
501                byteRead++;
502    
503                if (byteRead == 1)
504                    return firstByte;
505    
506                return wrappedStream.read();
507            }
508    
509            /**
510            * InputStreamReader is greedy and will try to read bytes in advance. We
511            * do NOT want this to happen since we use a temporary/"losing bytes"
512            * InputStreamReader above, that's why we hide the real
513            * wrappedStream.available() here.
514            */
515            public int available() {
516                return byteLength - byteRead;
517            }
518        }
519        
520    }