• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
3  *
4  * This software is distributable under the BSD license. See the terms of the
5  * BSD license in the documentation provided with this software.
6  */
7 package jline;
8 
9 import java.io.*;
10 
11 import jline.UnixTerminal.ReplayPrefixOneCharInputStream;
12 
13 /**
14  * <p>
15  * Terminal implementation for Microsoft Windows. Terminal initialization in
16  * {@link #initializeTerminal} is accomplished by extracting the
17  * <em>jline_<i>version</i>.dll</em>, saving it to the system temporary
18  * directoy (determined by the setting of the <em>java.io.tmpdir</em> System
19  * property), loading the library, and then calling the Win32 APIs <a
20  * href="http://msdn.microsoft.com/library/default.asp?
21  * url=/library/en-us/dllproc/base/setconsolemode.asp">SetConsoleMode</a> and
22  * <a href="http://msdn.microsoft.com/library/default.asp?
23  * url=/library/en-us/dllproc/base/getconsolemode.asp">GetConsoleMode</a> to
24  * disable character echoing.
25  * </p>
26  *
27  * <p>
28  * By default, the {@link #readCharacter} method will attempt to test to see if
29  * the specified {@link InputStream} is {@link System#in} or a wrapper around
30  * {@link FileDescriptor#in}, and if so, will bypass the character reading to
31  * directly invoke the readc() method in the JNI library. This is so the class
32  * can read special keys (like arrow keys) which are otherwise inaccessible via
33  * the {@link System#in} stream. Using JNI reading can be bypassed by setting
34  * the <code>jline.WindowsTerminal.directConsole</code> system property
35  * to <code>false</code>.
36  * </p>
37  *
38  * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
39  */
40 public class WindowsTerminal extends Terminal {
41     // constants copied from wincon.h
42 
43     /**
44      * The ReadFile or ReadConsole function returns only when a carriage return
45      * character is read. If this mode is disable, the functions return when one
46      * or more characters are available.
47      */
48     private static final int ENABLE_LINE_INPUT = 2;
49 
50     /**
51      * Characters read by the ReadFile or ReadConsole function are written to
52      * the active screen buffer as they are read. This mode can be used only if
53      * the ENABLE_LINE_INPUT mode is also enabled.
54      */
55     private static final int ENABLE_ECHO_INPUT = 4;
56 
57     /**
58      * CTRL+C is processed by the system and is not placed in the input buffer.
59      * If the input buffer is being read by ReadFile or ReadConsole, other
60      * control keys are processed by the system and are not returned in the
61      * ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also
62      * enabled, backspace, carriage return, and linefeed characters are handled
63      * by the system.
64      */
65     private static final int ENABLE_PROCESSED_INPUT = 1;
66 
67     /**
68      * User interactions that change the size of the console screen buffer are
69      * reported in the console's input buffee. Information about these events
70      * can be read from the input buffer by applications using
71      * theReadConsoleInput function, but not by those using ReadFile
72      * orReadConsole.
73      */
74     private static final int ENABLE_WINDOW_INPUT = 8;
75 
76     /**
77      * If the mouse pointer is within the borders of the console window and the
78      * window has the keyboard focus, mouse events generated by mouse movement
79      * and button presses are placed in the input buffer. These events are
80      * discarded by ReadFile or ReadConsole, even when this mode is enabled.
81      */
82     private static final int ENABLE_MOUSE_INPUT = 16;
83 
84     /**
85      * When enabled, text entered in a console window will be inserted at the
86      * current cursor location and all text following that location will not be
87      * overwritten. When disabled, all following text will be overwritten. An OR
88      * operation must be performed with this flag and the ENABLE_EXTENDED_FLAGS
89      * flag to enable this functionality.
90      */
91     private static final int ENABLE_PROCESSED_OUTPUT = 1;
92 
93     /**
94      * This flag enables the user to use the mouse to select and edit text. To
95      * enable this option, use the OR to combine this flag with
96      * ENABLE_EXTENDED_FLAGS.
97      */
98     private static final int ENABLE_WRAP_AT_EOL_OUTPUT = 2;
99 
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 
WindowsTerminal()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 
getConsoleMode()213     private native int getConsoleMode();
214 
setConsoleMode(final int mode)215     private native void setConsoleMode(final int mode);
216 
readByte()217     private native int readByte();
218 
getWindowsTerminalWidth()219     private native int getWindowsTerminalWidth();
220 
getWindowsTerminalHeight()221     private native int getWindowsTerminalHeight();
222 
readCharacter(final InputStream in)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 
initializeTerminal()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 
loadLibrary(final String name)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 
readVirtualKey(InputStream in)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 
isSupported()374     public boolean isSupported() {
375         return true;
376     }
377 
378     /**
379      * Windows doesn't support ANSI codes by default; disable them.
380      */
isANSISupported()381     public boolean isANSISupported() {
382         return false;
383     }
384 
getEcho()385     public boolean getEcho() {
386         return false;
387     }
388 
389     /**
390      * Unsupported; return the default.
391      *
392      * @see Terminal#getTerminalWidth
393      */
getTerminalWidth()394     public int getTerminalWidth() {
395         return getWindowsTerminalWidth();
396     }
397 
398     /**
399      * Unsupported; return the default.
400      *
401      * @see Terminal#getTerminalHeight
402      */
getTerminalHeight()403     public int getTerminalHeight() {
404         return getWindowsTerminalHeight();
405     }
406 
407     /**
408      * No-op for exceptions we want to silently consume.
409      */
consumeException(final Throwable e)410     private void consumeException(final Throwable e) {
411     }
412 
413     /**
414      * Whether or not to allow the use of the JNI console interaction.
415      */
setDirectConsole(Boolean directConsole)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      */
getDirectConsole()423     public Boolean getDirectConsole() {
424         return this.directConsole;
425     }
426 
isEchoEnabled()427     public synchronized boolean isEchoEnabled() {
428         return echoEnabled;
429     }
430 
enableEcho()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 
disableEcho()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 
getDefaultBindings()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 
ReplayPrefixOneCharInputStream(String encoding)464         public ReplayPrefixOneCharInputStream(String encoding) {
465             this.encoding = encoding;
466         }
467 
setInput(int recorded, InputStream wrapped)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 
setInputUTF8(int recorded, InputStream wrapped)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 
read()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         */
available()515         public int available() {
516             return byteLength - byteRead;
517         }
518     }
519 
520 }
521