1 /*
   2  * Copyright (c) 2002-2012, the original author or authors.
   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  * http://www.opensource.org/licenses/bsd-license.php
   8  */
   9 package jline;
  10 
  11 import java.io.FileDescriptor;
  12 import java.io.FileInputStream;
  13 import java.io.IOException;
  14 import java.io.InputStream;
  15 
  16 import jline.internal.Configuration;
  17 import jline.internal.Log;
  18 import org.fusesource.jansi.internal.WindowsSupport;
  19 import org.fusesource.jansi.internal.Kernel32;
  20 import static org.fusesource.jansi.internal.Kernel32.*;
  21 
  22 import static jline.WindowsTerminal.ConsoleMode.ENABLE_ECHO_INPUT;
  23 import static jline.WindowsTerminal.ConsoleMode.ENABLE_LINE_INPUT;
  24 import static jline.WindowsTerminal.ConsoleMode.ENABLE_PROCESSED_INPUT;
  25 import static jline.WindowsTerminal.ConsoleMode.ENABLE_WINDOW_INPUT;
  26 import static jline.internal.Preconditions.checkNotNull;
  27 
  28 /**
  29  * Terminal implementation for Microsoft Windows. Terminal initialization in
  30  * {@link #init} is accomplished by extracting the
  31  * <em>jline_<i>version</i>.dll</em>, saving it to the system temporary
  32  * directoy (determined by the setting of the <em>java.io.tmpdir</em> System
  33  * property), loading the library, and then calling the Win32 APIs <a
  34  * href="http://msdn.microsoft.com/library/default.asp?
  35  * url=/library/en-us/dllproc/base/setconsolemode.asp">SetConsoleMode</a> and
  36  * <a href="http://msdn.microsoft.com/library/default.asp?
  37  * url=/library/en-us/dllproc/base/getconsolemode.asp">GetConsoleMode</a> to
  38  * disable character echoing.
  39  * <p/>
  40  * <p>
  41  * By default, the {@link #wrapInIfNeeded(java.io.InputStream)} method will attempt
  42  * to test to see if the specified {@link InputStream} is {@link System#in} or a wrapper
  43  * around {@link FileDescriptor#in}, and if so, will bypass the character reading to
  44  * directly invoke the readc() method in the JNI library. This is so the class
  45  * can read special keys (like arrow keys) which are otherwise inaccessible via
  46  * the {@link System#in} stream. Using JNI reading can be bypassed by setting
  47  * the <code>jline.WindowsTerminal.directConsole</code> system property
  48  * to <code>false</code>.
  49  * </p>
  50  *
  51  * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
  52  * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
  53  * @since 2.0
  54  */
  55 public class WindowsTerminal
  56     extends TerminalSupport
  57 {
  58     public static final String DIRECT_CONSOLE = WindowsTerminal.class.getName() + ".directConsole";
  59 
  60     public static final String ANSI = WindowsTerminal.class.getName() + ".ansi";
  61 
  62     private boolean directConsole;
  63 
  64     private int originalMode;
  65 
  66     public WindowsTerminal() throws Exception {
  67         super(true);
  68     }
  69 
  70     @Override
  71     public void init() throws Exception {
  72         super.init();
  73 
  74         setAnsiSupported(Configuration.getBoolean(ANSI, true));
  75 
  76         //
  77         // FIXME: Need a way to disable direct console and sysin detection muck
  78         //
  79 
  80         setDirectConsole(Configuration.getBoolean(DIRECT_CONSOLE, true));
  81 
  82         this.originalMode = getConsoleMode();
  83         setConsoleMode(originalMode & ~ENABLE_ECHO_INPUT.code);
  84         setEchoEnabled(false);
  85     }
  86 
  87     /**
  88      * Restore the original terminal configuration, which can be used when
  89      * shutting down the console reader. The ConsoleReader cannot be
  90      * used after calling this method.
  91      */
  92     @Override
  93     public void restore() throws Exception {
  94         // restore the old console mode
  95         setConsoleMode(originalMode);
  96         super.restore();
  97     }
  98 
  99     @Override
 100     public int getWidth() {
 101         int w = getWindowsTerminalWidth();
 102         return w < 1 ? DEFAULT_WIDTH : w;
 103     }
 104 
 105     @Override
 106     public int getHeight() {
 107         int h = getWindowsTerminalHeight();
 108         return h < 1 ? DEFAULT_HEIGHT : h;
 109     }
 110 
 111     @Override
 112     public void setEchoEnabled(final boolean enabled) {
 113         // Must set these four modes at the same time to make it work fine.
 114         if (enabled) {
 115             setConsoleMode(getConsoleMode() |
 116                 ENABLE_ECHO_INPUT.code |
 117                 ENABLE_LINE_INPUT.code |
 118                 ENABLE_PROCESSED_INPUT.code |
 119                 ENABLE_WINDOW_INPUT.code);
 120         }
 121         else {
 122             setConsoleMode(getConsoleMode() &
 123                 ~(ENABLE_LINE_INPUT.code |
 124                     ENABLE_ECHO_INPUT.code |
 125                     ENABLE_PROCESSED_INPUT.code |
 126                     ENABLE_WINDOW_INPUT.code));
 127         }
 128         super.setEchoEnabled(enabled);
 129     }
 130 
 131     /**
 132      * Whether or not to allow the use of the JNI console interaction.
 133      */
 134     public void setDirectConsole(final boolean flag) {
 135         this.directConsole = flag;
 136         Log.debug("Direct console: ", flag);
 137     }
 138 
 139     /**
 140      * Whether or not to allow the use of the JNI console interaction.
 141      */
 142     public Boolean getDirectConsole() {
 143         return directConsole;
 144     }
 145 
 146 
 147     @Override
 148     public InputStream wrapInIfNeeded(InputStream in) throws IOException {
 149         if (directConsole && isSystemIn(in)) {
 150             return new InputStream() {
 151                 private byte[] buf = null;
 152                 int bufIdx = 0;
 153 
 154                 @Override
 155                 public int read() throws IOException {
 156                     while (buf == null || bufIdx == buf.length) {
 157                         buf = readConsoleInput();
 158                         bufIdx = 0;
 159                     }
 160                     int c = buf[bufIdx] & 0xFF;
 161                     bufIdx++;
 162                     return c;
 163                 }
 164             };
 165         } else {
 166             return super.wrapInIfNeeded(in);
 167         }
 168     }
 169 
 170     protected boolean isSystemIn(final InputStream in) throws IOException {
 171         if (in == null) {
 172             return false;
 173         }
 174         else if (in == System.in) {
 175             return true;
 176         }
 177         else if (in instanceof FileInputStream && ((FileInputStream) in).getFD() == FileDescriptor.in) {
 178             return true;
 179         }
 180 
 181         return false;
 182     }
 183 
 184     @Override
 185     public String getOutputEncoding() {
 186         int codepage = getConsoleOutputCodepage();
 187         //http://docs.oracle.com/javase/6/docs/technotes/guides/intl/encoding.doc.html
 188         String charsetMS = "ms" + codepage;
 189         if (java.nio.charset.Charset.isSupported(charsetMS)) {
 190             return charsetMS;
 191         }
 192         String charsetCP = "cp" + codepage;
 193         if (java.nio.charset.Charset.isSupported(charsetCP)) {
 194             return charsetCP;
 195         }
 196         Log.debug("can't figure out the Java Charset of this code page (" + codepage + ")...");
 197         return super.getOutputEncoding();
 198     }
 199 
 200     //
 201     // Native Bits
 202     //
 203     private int getConsoleMode() {
 204         return WindowsSupport.getConsoleMode();
 205     }
 206 
 207     private void setConsoleMode(int mode) {
 208         WindowsSupport.setConsoleMode(mode);
 209     }
 210 
 211     private byte[] readConsoleInput() {
 212         // XXX does how many events to read in one call matter?
 213         INPUT_RECORD[] events = null;
 214         try {
 215             events = WindowsSupport.readConsoleInput(1);
 216         } catch (IOException e) {
 217             Log.debug("read Windows console input error: ", e);
 218         }
 219         if (events == null) {
 220             return new byte[0];
 221         }
 222         StringBuilder sb = new StringBuilder();
 223         for (int i = 0; i < events.length; i++ ) {
 224             KEY_EVENT_RECORD keyEvent = events[i].keyEvent;
 225             //Log.trace(keyEvent.keyDown? "KEY_DOWN" : "KEY_UP", "key code:", keyEvent.keyCode, "char:", (long)keyEvent.uchar); 
 226             if (keyEvent.keyDown) {
 227                 if (keyEvent.uchar > 0) {
 228                     // support some C1 control sequences: ALT + [@-_] (and [a-z]?) => ESC <ascii>
 229                     // http://en.wikipedia.org/wiki/C0_and_C1_control_codes#C1_set
 230                     final int altState = KEY_EVENT_RECORD.LEFT_ALT_PRESSED | KEY_EVENT_RECORD.RIGHT_ALT_PRESSED;
 231                     // Pressing "Alt Gr" is translated to Alt-Ctrl, hence it has to be checked that Ctrl is _not_ pressed,
 232                     // otherwise inserting of "Alt Gr" codes on non-US keyboards would yield errors
 233                     final int ctrlState = KEY_EVENT_RECORD.LEFT_CTRL_PRESSED | KEY_EVENT_RECORD.RIGHT_CTRL_PRESSED;
 234                     if (((keyEvent.uchar >= '@' && keyEvent.uchar <= '_') || (keyEvent.uchar >= 'a' && keyEvent.uchar <= 'z'))
 235                         && ((keyEvent.controlKeyState & altState) != 0) && ((keyEvent.controlKeyState & ctrlState) == 0)) {
 236                         sb.append('\u001B'); // ESC
 237                     }
 238 
 239                     sb.append(keyEvent.uchar);
 240                     continue;
 241                 }
 242                 // virtual keycodes: http://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
 243                 // just add support for basic editing keys (no control state, no numpad keys)
 244                 String escapeSequence = null;
 245                 switch (keyEvent.keyCode) {
 246                 case 0x21: // VK_PRIOR PageUp
 247                     escapeSequence = "\u001B[5~";
 248                     break;
 249                 case 0x22: // VK_NEXT PageDown
 250                     escapeSequence = "\u001B[6~";
 251                     break;
 252                 case 0x23: // VK_END
 253                     escapeSequence = "\u001B[4~";
 254                     break;
 255                 case 0x24: // VK_HOME
 256                     escapeSequence = "\u001B[1~";
 257                     break;
 258                 case 0x25: // VK_LEFT
 259                     escapeSequence = "\u001B[D";
 260                     break;
 261                 case 0x26: // VK_UP
 262                     escapeSequence = "\u001B[A";
 263                     break;
 264                 case 0x27: // VK_RIGHT
 265                     escapeSequence = "\u001B[C";
 266                     break;
 267                 case 0x28: // VK_DOWN
 268                     escapeSequence = "\u001B[B";
 269                     break;
 270                 case 0x2D: // VK_INSERT
 271                     escapeSequence = "\u001B[2~";
 272                     break;
 273                 case 0x2E: // VK_DELETE
 274                     escapeSequence = "\u001B[3~";
 275                     break;
 276                 default:
 277                     break;
 278                 }
 279                 if (escapeSequence != null) {
 280                     for (int k = 0; k < keyEvent.repeatCount; k++) {
 281                         sb.append(escapeSequence);
 282                     }
 283                 }
 284             } else {
 285                 // key up event
 286                 // support ALT+NumPad input method
 287                 if (keyEvent.keyCode == 0x12/*VK_MENU ALT key*/ && keyEvent.uchar > 0) {
 288                     sb.append(keyEvent.uchar);
 289                 }
 290             }
 291         }
 292         return sb.toString().getBytes();
 293     }
 294 
 295     private int getConsoleOutputCodepage() {
 296         return Kernel32.GetConsoleOutputCP();
 297     }
 298 
 299     private int getWindowsTerminalWidth() {
 300         return WindowsSupport.getWindowsTerminalWidth();
 301     }
 302 
 303     private int getWindowsTerminalHeight() {
 304         return WindowsSupport.getWindowsTerminalHeight();
 305     }
 306 
 307     /**
 308      * Console mode
 309      * <p/>
 310      * Constants copied <tt>wincon.h</tt>.
 311      */
 312     public static enum ConsoleMode
 313     {
 314         /**
 315          * The ReadFile or ReadConsole function returns only when a carriage return
 316          * character is read. If this mode is disable, the functions return when one
 317          * or more characters are available.
 318          */
 319         ENABLE_LINE_INPUT(2),
 320 
 321         /**
 322          * Characters read by the ReadFile or ReadConsole function are written to
 323          * the active screen buffer as they are read. This mode can be used only if
 324          * the ENABLE_LINE_INPUT mode is also enabled.
 325          */
 326         ENABLE_ECHO_INPUT(4),
 327 
 328         /**
 329          * CTRL+C is processed by the system and is not placed in the input buffer.
 330          * If the input buffer is being read by ReadFile or ReadConsole, other
 331          * control keys are processed by the system and are not returned in the
 332          * ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also
 333          * enabled, backspace, carriage return, and linefeed characters are handled
 334          * by the system.
 335          */
 336         ENABLE_PROCESSED_INPUT(1),
 337 
 338         /**
 339          * User interactions that change the size of the console screen buffer are
 340          * reported in the console's input buffee. Information about these events
 341          * can be read from the input buffer by applications using
 342          * theReadConsoleInput function, but not by those using ReadFile
 343          * orReadConsole.
 344          */
 345         ENABLE_WINDOW_INPUT(8),
 346 
 347         /**
 348          * If the mouse pointer is within the borders of the console window and the
 349          * window has the keyboard focus, mouse events generated by mouse movement
 350          * and button presses are placed in the input buffer. These events are
 351          * discarded by ReadFile or ReadConsole, even when this mode is enabled.
 352          */
 353         ENABLE_MOUSE_INPUT(16),
 354 
 355         /**
 356          * When enabled, text entered in a console window will be inserted at the
 357          * current cursor location and all text following that location will not be
 358          * overwritten. When disabled, all following text will be overwritten. An OR
 359          * operation must be performed with this flag and the ENABLE_EXTENDED_FLAGS
 360          * flag to enable this functionality.
 361          */
 362         ENABLE_PROCESSED_OUTPUT(1),
 363 
 364         /**
 365          * This flag enables the user to use the mouse to select and edit text. To
 366          * enable this option, use the OR to combine this flag with
 367          * ENABLE_EXTENDED_FLAGS.
 368          */
 369         ENABLE_WRAP_AT_EOL_OUTPUT(2),;
 370 
 371         public final int code;
 372 
 373         ConsoleMode(final int code) {
 374             this.code = code;
 375         }
 376     }
 377 
 378 }