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.disableDirectConsole</code> system property
035 * to <code>true</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 = getClass().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(getClass()
290 .getResourceAsStream(name + bits + ".dll"));
291
292 try {
293 OutputStream 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 fout.close();
302 } catch (IOException ioe) {
303 // We might get an IOException trying to overwrite an existing
304 // jline.dll file if there is another process using the DLL.
305 // If this happens, ignore errors.
306 if (!exists) {
307 throw ioe;
308 }
309 }
310
311 // try to clean up the DLL after the JVM exits
312 f.deleteOnExit();
313
314 // now actually load the DLL
315 System.load(f.getAbsolutePath());
316 }
317
318 public int readVirtualKey(InputStream in) throws IOException {
319 int indicator = readCharacter(in);
320
321 // in Windows terminals, arrow keys are represented by
322 // a sequence of 2 characters. E.g., the up arrow
323 // key yields 224, 72
324 if (indicator == SPECIAL_KEY_INDICATOR
325 || indicator == NUMPAD_KEY_INDICATOR) {
326 int key = readCharacter(in);
327
328 switch (key) {
329 case UP_ARROW_KEY:
330 return CTRL_P; // translate UP -> CTRL-P
331 case LEFT_ARROW_KEY:
332 return CTRL_B; // translate LEFT -> CTRL-B
333 case RIGHT_ARROW_KEY:
334 return CTRL_F; // translate RIGHT -> CTRL-F
335 case DOWN_ARROW_KEY:
336 return CTRL_N; // translate DOWN -> CTRL-N
337 case DELETE_KEY:
338 return CTRL_QM; // translate DELETE -> CTRL-?
339 case HOME_KEY:
340 return CTRL_A;
341 case END_KEY:
342 return CTRL_E;
343 case PAGE_UP_KEY:
344 return CTRL_K;
345 case PAGE_DOWN_KEY:
346 return CTRL_L;
347 case ESCAPE_KEY:
348 return CTRL_OB; // translate ESCAPE -> CTRL-[
349 case INSERT_KEY:
350 return CTRL_C;
351 default:
352 return 0;
353 }
354 } else if (indicator > 128) {
355 // handle unicode characters longer than 2 bytes,
356 // thanks to Marc.Herbert@continuent.com
357 replayStream.setInput(indicator, in);
358 // replayReader = new InputStreamReader(replayStream, encoding);
359 indicator = replayReader.read();
360
361 }
362
363 return indicator;
364
365 }
366
367 public boolean isSupported() {
368 return true;
369 }
370
371 /**
372 * Windows doesn't support ANSI codes by default; disable them.
373 */
374 public boolean isANSISupported() {
375 return false;
376 }
377
378 public boolean getEcho() {
379 return false;
380 }
381
382 /**
383 * Unsupported; return the default.
384 *
385 * @see Terminal#getTerminalWidth
386 */
387 public int getTerminalWidth() {
388 return getWindowsTerminalWidth();
389 }
390
391 /**
392 * Unsupported; return the default.
393 *
394 * @see Terminal#getTerminalHeight
395 */
396 public int getTerminalHeight() {
397 return getWindowsTerminalHeight();
398 }
399
400 /**
401 * No-op for exceptions we want to silently consume.
402 */
403 private void consumeException(final Throwable e) {
404 }
405
406 /**
407 * Whether or not to allow the use of the JNI console interaction.
408 */
409 public void setDirectConsole(Boolean directConsole) {
410 this.directConsole = directConsole;
411 }
412
413 /**
414 * Whether or not to allow the use of the JNI console interaction.
415 */
416 public Boolean getDirectConsole() {
417 return this.directConsole;
418 }
419
420 public synchronized boolean isEchoEnabled() {
421 return echoEnabled;
422 }
423
424 public synchronized void enableEcho() {
425 // Must set these four modes at the same time to make it work fine.
426 setConsoleMode(getConsoleMode() | ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT
427 | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT);
428 echoEnabled = true;
429 }
430
431 public synchronized void disableEcho() {
432 // Must set these four modes at the same time to make it work fine.
433 setConsoleMode(getConsoleMode()
434 & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT
435 | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT));
436 echoEnabled = true;
437 }
438
439 public InputStream getDefaultBindings() {
440 return getClass().getResourceAsStream("windowsbindings.properties");
441 }
442
443 /**
444 * This is awkward and inefficient, but probably the minimal way to add
445 * UTF-8 support to JLine
446 *
447 * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
448 */
449 static class ReplayPrefixOneCharInputStream extends InputStream {
450 byte firstByte;
451 int byteLength;
452 InputStream wrappedStream;
453 int byteRead;
454
455 final String encoding;
456
457 public ReplayPrefixOneCharInputStream(String encoding) {
458 this.encoding = encoding;
459 }
460
461 public void setInput(int recorded, InputStream wrapped) throws IOException {
462 this.byteRead = 0;
463 this.firstByte = (byte) recorded;
464 this.wrappedStream = wrapped;
465
466 byteLength = 1;
467 if (encoding.equalsIgnoreCase("UTF-8"))
468 setInputUTF8(recorded, wrapped);
469 else if (encoding.equalsIgnoreCase("UTF-16"))
470 byteLength = 2;
471 else if (encoding.equalsIgnoreCase("UTF-32"))
472 byteLength = 4;
473 }
474
475
476 public void setInputUTF8(int recorded, InputStream wrapped) throws IOException {
477 // 110yyyyy 10zzzzzz
478 if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
479 this.byteLength = 2;
480 // 1110xxxx 10yyyyyy 10zzzzzz
481 else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
482 this.byteLength = 3;
483 // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
484 else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
485 this.byteLength = 4;
486 else
487 throw new IOException("invalid UTF-8 first byte: " + firstByte);
488 }
489
490 public int read() throws IOException {
491 if (available() == 0)
492 return -1;
493
494 byteRead++;
495
496 if (byteRead == 1)
497 return firstByte;
498
499 return wrappedStream.read();
500 }
501
502 /**
503 * InputStreamReader is greedy and will try to read bytes in advance. We
504 * do NOT want this to happen since we use a temporary/"losing bytes"
505 * InputStreamReader above, that's why we hide the real
506 * wrappedStream.available() here.
507 */
508 public int available() {
509 return byteLength - byteRead;
510 }
511 }
512
513 }