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 }