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 }