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 */
7package jline;
8
9import java.io.*;
10
11import 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 */
40public 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
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
213    private native int getConsoleMode();
214
215    private native void setConsoleMode(final int mode);
216
217    private native int readByte();
218
219    private native int getWindowsTerminalWidth();
220
221    private native int getWindowsTerminalHeight();
222
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
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
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
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
374    public boolean isSupported() {
375        return true;
376    }
377
378    /**
379     * Windows doesn't support ANSI codes by default; disable them.
380     */
381    public boolean isANSISupported() {
382        return false;
383    }
384
385    public boolean getEcho() {
386        return false;
387    }
388
389    /**
390     * Unsupported; return the default.
391     *
392     * @see Terminal#getTerminalWidth
393     */
394    public int getTerminalWidth() {
395        return getWindowsTerminalWidth();
396    }
397
398    /**
399     * Unsupported; return the default.
400     *
401     * @see Terminal#getTerminalHeight
402     */
403    public int getTerminalHeight() {
404        return getWindowsTerminalHeight();
405    }
406
407    /**
408     * No-op for exceptions we want to silently consume.
409     */
410    private void consumeException(final Throwable e) {
411    }
412
413    /**
414     * Whether or not to allow the use of the JNI console interaction.
415     */
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     */
423    public Boolean getDirectConsole() {
424        return this.directConsole;
425    }
426
427    public synchronized boolean isEchoEnabled() {
428        return echoEnabled;
429    }
430
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
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
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
464        public ReplayPrefixOneCharInputStream(String encoding) {
465            this.encoding = encoding;
466        }
467
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
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
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        */
515        public int available() {
516            return byteLength - byteRead;
517        }
518    }
519
520}
521