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(
193: "jline.WindowsTerminal.input.encoding", System
194: .getProperty("file.encoding"));
195: ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(
196: encoding);
197: InputStreamReader replayReader;
198:
199: public WindowsTerminal() {
200: String dir = System
201: .getProperty("jline.WindowsTerminal.directConsole");
202:
203: if ("true".equals(dir)) {
204: directConsole = Boolean.TRUE;
205: } else if ("false".equals(dir)) {
206: directConsole = Boolean.FALSE;
207: }
208:
209: try {
210: replayReader = new InputStreamReader(replayStream, encoding);
211: } catch (Exception e) {
212: throw new RuntimeException(e);
213: }
214:
215: }
216:
217: private native int getConsoleMode();
218:
219: private native void setConsoleMode(final int mode);
220:
221: private native int readByte();
222:
223: private native int getWindowsTerminalWidth();
224:
225: private native int getWindowsTerminalHeight();
226:
227: public int readCharacter(final InputStream in) throws IOException {
228: // if we can detect that we are directly wrapping the system
229: // input, then bypass the input stream and read directly (which
230: // allows us to access otherwise unreadable strokes, such as
231: // the arrow keys)
232: if (directConsole == Boolean.FALSE) {
233: return super .readCharacter(in);
234: } else if ((directConsole == Boolean.TRUE)
235: || ((in == System.in) || (in instanceof FileInputStream && (((FileInputStream) in)
236: .getFD() == FileDescriptor.in)))) {
237: return readByte();
238: } else {
239: return super .readCharacter(in);
240: }
241: }
242:
243: public void initializeTerminal() throws Exception {
244: loadLibrary("jline");
245:
246: final int originalMode = getConsoleMode();
247:
248: setConsoleMode(originalMode & ~ENABLE_ECHO_INPUT);
249:
250: // set the console to raw mode
251: int newMode = originalMode
252: & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT
253: | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT);
254: echoEnabled = false;
255: setConsoleMode(newMode);
256:
257: // at exit, restore the original tty configuration (for JDK 1.3+)
258: try {
259: Runtime.getRuntime().addShutdownHook(new Thread() {
260: public void start() {
261: // restore the old console mode
262: setConsoleMode(originalMode);
263: }
264: });
265: } catch (AbstractMethodError ame) {
266: // JDK 1.3+ only method. Bummer.
267: consumeException(ame);
268: }
269: }
270:
271: private void loadLibrary(final String name) throws IOException {
272: // store the DLL in the temporary directory for the System
273: String version = getClass().getPackage()
274: .getImplementationVersion();
275:
276: if (version == null) {
277: version = "";
278: }
279:
280: version = version.replace('.', '_');
281:
282: File f = new File(System.getProperty("java.io.tmpdir"), name
283: + "_" + version + ".dll");
284: boolean exists = f.isFile(); // check if it already exists
285:
286: // extract the embedded jline.dll file from the jar and save
287: // it to the current directory
288: int bits = 32;
289:
290: // check for 64-bit systems and use to appropriate DLL
291: if (System.getProperty("os.arch").indexOf("64") != -1)
292: bits = 64;
293:
294: InputStream in = new BufferedInputStream(getClass()
295: .getResourceAsStream(name + bits + ".dll"));
296:
297: try {
298: OutputStream fout = new BufferedOutputStream(
299: new FileOutputStream(f));
300: byte[] bytes = new byte[1024 * 10];
301:
302: for (int n = 0; n != -1; n = in.read(bytes)) {
303: fout.write(bytes, 0, n);
304: }
305:
306: fout.close();
307: } catch (IOException ioe) {
308: // We might get an IOException trying to overwrite an existing
309: // jline.dll file if there is another process using the DLL.
310: // If this happens, ignore errors.
311: if (!exists) {
312: throw ioe;
313: }
314: }
315:
316: // try to clean up the DLL after the JVM exits
317: f.deleteOnExit();
318:
319: // now actually load the DLL
320: System.load(f.getAbsolutePath());
321: }
322:
323: public int readVirtualKey(InputStream in) throws IOException {
324: int indicator = readCharacter(in);
325:
326: // in Windows terminals, arrow keys are represented by
327: // a sequence of 2 characters. E.g., the up arrow
328: // key yields 224, 72
329: if (indicator == SPECIAL_KEY_INDICATOR
330: || indicator == NUMPAD_KEY_INDICATOR) {
331: int key = readCharacter(in);
332:
333: switch (key) {
334: case UP_ARROW_KEY:
335: return CTRL_P; // translate UP -> CTRL-P
336: case LEFT_ARROW_KEY:
337: return CTRL_B; // translate LEFT -> CTRL-B
338: case RIGHT_ARROW_KEY:
339: return CTRL_F; // translate RIGHT -> CTRL-F
340: case DOWN_ARROW_KEY:
341: return CTRL_N; // translate DOWN -> CTRL-N
342: case DELETE_KEY:
343: return CTRL_QM; // translate DELETE -> CTRL-?
344: case HOME_KEY:
345: return CTRL_A;
346: case END_KEY:
347: return CTRL_E;
348: case PAGE_UP_KEY:
349: return CTRL_K;
350: case PAGE_DOWN_KEY:
351: return CTRL_L;
352: case ESCAPE_KEY:
353: return CTRL_OB; // translate ESCAPE -> CTRL-[
354: case INSERT_KEY:
355: return CTRL_C;
356: default:
357: return 0;
358: }
359: } else if (indicator > 128) {
360: // handle unicode characters longer than 2 bytes,
361: // thanks to Marc.Herbert@continuent.com
362: replayStream.setInput(indicator, in);
363: // replayReader = new InputStreamReader(replayStream, encoding);
364: indicator = replayReader.read();
365:
366: }
367:
368: return indicator;
369:
370: }
371:
372: public boolean isSupported() {
373: return true;
374: }
375:
376: /**
377: * Windows doesn't support ANSI codes by default; disable them.
378: */
379: public boolean isANSISupported() {
380: return false;
381: }
382:
383: public boolean getEcho() {
384: return false;
385: }
386:
387: /**
388: * Unsupported; return the default.
389: *
390: * @see Terminal#getTerminalWidth
391: */
392: public int getTerminalWidth() {
393: return getWindowsTerminalWidth();
394: }
395:
396: /**
397: * Unsupported; return the default.
398: *
399: * @see Terminal#getTerminalHeight
400: */
401: public int getTerminalHeight() {
402: return getWindowsTerminalHeight();
403: }
404:
405: /**
406: * No-op for exceptions we want to silently consume.
407: */
408: private void consumeException(final Throwable e) {
409: }
410:
411: /**
412: * Whether or not to allow the use of the JNI console interaction.
413: */
414: public void setDirectConsole(Boolean directConsole) {
415: this .directConsole = directConsole;
416: }
417:
418: /**
419: * Whether or not to allow the use of the JNI console interaction.
420: */
421: public Boolean getDirectConsole() {
422: return this .directConsole;
423: }
424:
425: public synchronized boolean isEchoEnabled() {
426: return echoEnabled;
427: }
428:
429: public synchronized void enableEcho() {
430: // Must set these four modes at the same time to make it work fine.
431: setConsoleMode(getConsoleMode() | ENABLE_ECHO_INPUT
432: | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT
433: | ENABLE_WINDOW_INPUT);
434: echoEnabled = true;
435: }
436:
437: public synchronized void disableEcho() {
438: // Must set these four modes at the same time to make it work fine.
439: setConsoleMode(getConsoleMode()
440: & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT
441: | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT));
442: echoEnabled = true;
443: }
444:
445: public InputStream getDefaultBindings() {
446: return getClass().getResourceAsStream(
447: "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:
464: public ReplayPrefixOneCharInputStream(String encoding) {
465: this .encoding = encoding;
466: }
467:
468: public void setInput(int recorded, InputStream wrapped)
469: throws IOException {
470: this .byteRead = 0;
471: this .firstByte = (byte) recorded;
472: this .wrappedStream = wrapped;
473:
474: byteLength = 1;
475: if (encoding.equalsIgnoreCase("UTF-8"))
476: setInputUTF8(recorded, wrapped);
477: else if (encoding.equalsIgnoreCase("UTF-16"))
478: byteLength = 2;
479: else if (encoding.equalsIgnoreCase("UTF-32"))
480: byteLength = 4;
481: }
482:
483: public void setInputUTF8(int recorded, InputStream wrapped)
484: throws IOException {
485: // 110yyyyy 10zzzzzz
486: if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
487: this .byteLength = 2;
488: // 1110xxxx 10yyyyyy 10zzzzzz
489: else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
490: this .byteLength = 3;
491: // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
492: else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
493: this .byteLength = 4;
494: else
495: throw new IOException("invalid UTF-8 first byte: "
496: + firstByte);
497: }
498:
499: public int read() throws IOException {
500: if (available() == 0)
501: return -1;
502:
503: byteRead++;
504:
505: if (byteRead == 1)
506: return firstByte;
507:
508: return wrappedStream.read();
509: }
510:
511: /**
512: * InputStreamReader is greedy and will try to read bytes in advance. We
513: * do NOT want this to happen since we use a temporary/"losing bytes"
514: * InputStreamReader above, that's why we hide the real
515: * wrappedStream.available() here.
516: */
517: public int available() {
518: return byteLength - byteRead;
519: }
520: }
521:
522: }
|