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