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    
011    /**
012     *  <p>
013     *  Terminal implementation for Microsoft Windows. Terminal initialization
014     *  in {@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>
017     *  System property), loading the library, and then calling the Win32 APIs
018     *  <a href="http://msdn.microsoft.com/library/default.asp?
019     *  url=/library/en-us/dllproc/base/setconsolemode.asp">SetConsoleMode</a>
020     *  and
021     *  <a href="http://msdn.microsoft.com/library/default.asp?
022     *  url=/library/en-us/dllproc/base/getconsolemode.asp">GetConsoleMode</a>
023     *  to disable character echoing.
024     *  </p>
025     *
026     *  <p>
027     *  By default, the {@link #readCharacter} method will attempt to test
028     *  to see if the specified {@link InputStream} is {@link System#in}
029     *  or a wrapper around {@link FileDescriptor#in}, and if so, will
030     *  bypass the character reading to directly invoke the
031     *  readc() method in the JNI library. This is so the class can
032     *  read special keys (like arrow keys) which are otherwise
033     *  inaccessible via the {@link System#in} stream. Using JNI
034     *  reading can be bypassed by setting the
035     *  <code>jline.WindowsTerminal.disableDirectConsole</code> system
036     *  property to <code>true</code>.
037     *  </p>
038     *
039     *  @author  <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
040     */
041    public class WindowsTerminal extends Terminal {
042        // constants copied from wincon.h
043    
044        /**
045         *  The ReadFile or ReadConsole function returns only when
046         *  a carriage return character is read. If this mode is disable,
047         *  the functions return when one or more characters are
048         *  available.
049         */
050        private static final int ENABLE_LINE_INPUT = 2;
051    
052        /**
053         *  Characters read by the ReadFile or ReadConsole function
054         *  are written to the active screen buffer as they are read.
055         *  This mode can be used only if the ENABLE_LINE_INPUT mode
056         *  is also enabled.
057         */
058        private static final int ENABLE_ECHO_INPUT = 4;
059    
060        /**
061         *  CTRL+C is processed by the system and is not placed
062         *  in the input buffer. If the input buffer is being read
063         *  by ReadFile or ReadConsole, other control keys are processed
064         *  by the system and are not returned in the ReadFile or ReadConsole
065         *  buffer. If the ENABLE_LINE_INPUT mode is also enabled,
066         *  backspace, carriage return, and linefeed characters are
067         *  handled by the system.
068         */
069        private static final int ENABLE_PROCESSED_INPUT = 1;
070    
071        /**
072         *  User interactions that change the size of the console
073         *  screen buffer are reported in the console's input buffee.
074         *  Information about these events can be read from the input
075         *  buffer by applications using theReadConsoleInput function,
076         *  but not by those using ReadFile orReadConsole.
077         */
078        private static final int ENABLE_WINDOW_INPUT = 8;
079    
080        /**
081         *  If the mouse pointer is within the borders of the console
082         *  window and the window has the keyboard focus, mouse events
083         *  generated by mouse movement and button presses are placed
084         *  in the input buffer. These events are discarded by ReadFile
085         *  or ReadConsole, even when this mode is enabled.
086         */
087        private static final int ENABLE_MOUSE_INPUT = 16;
088    
089        /**
090         *  When enabled, text entered in a console window will
091         *  be inserted at the current cursor location and all text
092         *  following that location will not be overwritten. When disabled,
093         *  all following text will be overwritten. An OR operation
094         *  must be performed with this flag and the ENABLE_EXTENDED_FLAGS
095         *  flag to enable this functionality.
096         */
097        private static final int ENABLE_PROCESSED_OUTPUT = 1;
098    
099        /**
100         *  This flag enables the user to use the mouse to select
101         *  and edit text. To enable this option, use the OR to combine
102         *  this flag with ENABLE_EXTENDED_FLAGS.
103         */
104        private static final int ENABLE_WRAP_AT_EOL_OUTPUT = 2;
105        private Boolean directConsole;
106    
107        public WindowsTerminal() {
108            String dir = System.getProperty("jline.WindowsTerminal.directConsole");
109    
110            if ("true".equals(dir)) {
111                directConsole = Boolean.TRUE;
112            } else if ("false".equals(dir)) {
113                directConsole = Boolean.FALSE;
114            }
115        }
116    
117        private native int getConsoleMode();
118    
119        private native void setConsoleMode(final int mode);
120    
121        private native int readByte();
122    
123        private native int getWindowsTerminalWidth();
124    
125        private native int getWindowsTerminalHeight();
126    
127        public int readCharacter(final InputStream in) throws IOException {
128            // if we can detect that we are directly wrapping the system
129            // input, then bypass the input stream and read directly (which
130            // allows us to access otherwise unreadable strokes, such as
131            // the arrow keys)
132            if (directConsole == Boolean.FALSE) {
133                return super.readCharacter(in);
134            } else if ((directConsole == Boolean.TRUE)
135                || ((in == System.in) || (in instanceof FileInputStream
136                    && (((FileInputStream) in).getFD() == FileDescriptor.in)))) {
137                return readByte();
138            } else {
139                return super.readCharacter(in);
140            }
141        }
142    
143        public void initializeTerminal() throws Exception {
144            loadLibrary("jline");
145    
146            final int originalMode = getConsoleMode();
147    
148            setConsoleMode(originalMode & ~ENABLE_ECHO_INPUT);
149    
150            // set the console to raw mode
151            int newMode =
152                originalMode
153                & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT
154                | ENABLE_WINDOW_INPUT);
155            setConsoleMode(newMode);
156    
157            // at exit, restore the original tty configuration (for JDK 1.3+)
158            try {
159                Runtime.getRuntime().addShutdownHook(new Thread() {
160                        public void start() {
161                            // restore the old console mode
162                            setConsoleMode(originalMode);
163                        }
164                    });
165            } catch (AbstractMethodError ame) {
166                // JDK 1.3+ only method. Bummer.
167                consumeException(ame);
168            }
169        }
170    
171        private void loadLibrary(final String name) throws IOException {
172            // store the DLL in the temporary directory for the System
173            String version = getClass().getPackage().getImplementationVersion();
174    
175            if (version == null) {
176                version = "";
177            }
178    
179            version = version.replace('.', '_');
180    
181            File f =
182                new File(System.getProperty("java.io.tmpdir"),
183                         name + "_" + version + ".dll");
184            boolean exists = f.isFile(); // check if it already exists
185    
186            // extract the embedded jline.dll file from the jar and save
187            // it to the current directory
188            InputStream in =
189                new BufferedInputStream(getClass().getResourceAsStream(name
190                                                                       + ".dll"));
191    
192            try {
193                OutputStream fout =
194                    new BufferedOutputStream(new FileOutputStream(f));
195                byte[] bytes = new byte[1024 * 10];
196    
197                for (int n = 0; n != -1; n = in.read(bytes)) {
198                    fout.write(bytes, 0, n);
199                }
200    
201                fout.close();
202            } catch (IOException ioe) {
203                // We might get an IOException trying to overwrite an existing
204                // jline.dll file if there is another process using the DLL.
205                // If this happens, ignore errors.
206                if (!exists) {
207                    throw ioe;
208                }
209            }
210    
211            // try to clean up the DLL after the JVM exits
212            f.deleteOnExit();
213    
214            // now actually load the DLL
215            System.load(f.getAbsolutePath());
216        }
217    
218        public int readVirtualKey(InputStream in) throws IOException {
219            int c = readCharacter(in);
220    
221            // in Windows terminals, arrow keys are represented by
222            // a sequence of 2 characters. E.g., the up arrow
223            // key yields 224, 72
224            if (c == 224) {
225                c = readCharacter(in);
226    
227                if (c == 72) {
228                    return CTRL_P; // translate UP -> CTRL-P
229                } else if (c == 80) {
230                    return CTRL_N; // translate DOWN -> CTRL-N
231                } else if (c == 75) {
232                    return CTRL_B; // translate LEFT -> CTRL-B
233                } else if (c == 77) {
234                    return CTRL_F; // translate RIGHT -> CTRL-F
235                }
236            }
237    
238            return c;
239        }
240    
241        public boolean isSupported() {
242            return true;
243        }
244    
245        /**
246         *  Windows doesn't support ANSI codes by default; disable them.
247         */
248        public boolean isANSISupported() {
249            return false;
250        }
251    
252        public boolean getEcho() {
253            return false;
254        }
255    
256        /**
257         *  Unsupported; return the default.
258         *
259         *  @see Terminal#getTerminalWidth
260         */
261        public int getTerminalWidth() {
262            return getWindowsTerminalWidth();
263        }
264    
265        /**
266         *  Unsupported; return the default.
267         *
268         *  @see Terminal#getTerminalHeight
269         */
270        public int getTerminalHeight() {
271            return getWindowsTerminalHeight();
272        }
273    
274        /**
275         *  No-op for exceptions we want to silently consume.
276         */
277        private void consumeException(final Throwable e) {
278        }
279    
280        /**
281         *  Whether or not to allow the use of the JNI console interaction.
282         */
283        public void setDirectConsole(Boolean directConsole) {
284            this.directConsole = directConsole;
285        }
286    
287        /**
288         *  Whether or not to allow the use of the JNI console interaction.
289         */
290        public Boolean getDirectConsole() {
291            return this.directConsole;
292        }
293    }