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