View Javadoc

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 }