summaryrefslogtreecommitdiff
path: root/src/src/main/java/jline/WindowsTerminal.java
blob: d036088283fdf49e248a45cdc64911f950922cc0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
/*
 * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 */
package jline;

import java.io.*;

import jline.UnixTerminal.ReplayPrefixOneCharInputStream;

/**
 * <p>
 * Terminal implementation for Microsoft Windows. Terminal initialization in
 * {@link #initializeTerminal} is accomplished by extracting the
 * <em>jline_<i>version</i>.dll</em>, saving it to the system temporary
 * directoy (determined by the setting of the <em>java.io.tmpdir</em> System
 * property), loading the library, and then calling the Win32 APIs <a
 * href="http://msdn.microsoft.com/library/default.asp?
 * url=/library/en-us/dllproc/base/setconsolemode.asp">SetConsoleMode</a> and
 * <a href="http://msdn.microsoft.com/library/default.asp?
 * url=/library/en-us/dllproc/base/getconsolemode.asp">GetConsoleMode</a> to
 * disable character echoing.
 * </p>
 *
 * <p>
 * By default, the {@link #readCharacter} method will attempt to test to see if
 * the specified {@link InputStream} is {@link System#in} or a wrapper around
 * {@link FileDescriptor#in}, and if so, will bypass the character reading to
 * directly invoke the readc() method in the JNI library. This is so the class
 * can read special keys (like arrow keys) which are otherwise inaccessible via
 * the {@link System#in} stream. Using JNI reading can be bypassed by setting
 * the <code>jline.WindowsTerminal.directConsole</code> system property
 * to <code>false</code>.
 * </p>
 *
 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
 */
public class WindowsTerminal extends Terminal {
    // constants copied from wincon.h

    /**
     * The ReadFile or ReadConsole function returns only when a carriage return
     * character is read. If this mode is disable, the functions return when one
     * or more characters are available.
     */
    private static final int ENABLE_LINE_INPUT = 2;

    /**
     * Characters read by the ReadFile or ReadConsole function are written to
     * the active screen buffer as they are read. This mode can be used only if
     * the ENABLE_LINE_INPUT mode is also enabled.
     */
    private static final int ENABLE_ECHO_INPUT = 4;

    /**
     * CTRL+C is processed by the system and is not placed in the input buffer.
     * If the input buffer is being read by ReadFile or ReadConsole, other
     * control keys are processed by the system and are not returned in the
     * ReadFile or ReadConsole buffer. If the ENABLE_LINE_INPUT mode is also
     * enabled, backspace, carriage return, and linefeed characters are handled
     * by the system.
     */
    private static final int ENABLE_PROCESSED_INPUT = 1;

    /**
     * User interactions that change the size of the console screen buffer are
     * reported in the console's input buffee. Information about these events
     * can be read from the input buffer by applications using
     * theReadConsoleInput function, but not by those using ReadFile
     * orReadConsole.
     */
    private static final int ENABLE_WINDOW_INPUT = 8;

    /**
     * If the mouse pointer is within the borders of the console window and the
     * window has the keyboard focus, mouse events generated by mouse movement
     * and button presses are placed in the input buffer. These events are
     * discarded by ReadFile or ReadConsole, even when this mode is enabled.
     */
    private static final int ENABLE_MOUSE_INPUT = 16;

    /**
     * When enabled, text entered in a console window will be inserted at the
     * current cursor location and all text following that location will not be
     * overwritten. When disabled, all following text will be overwritten. An OR
     * operation must be performed with this flag and the ENABLE_EXTENDED_FLAGS
     * flag to enable this functionality.
     */
    private static final int ENABLE_PROCESSED_OUTPUT = 1;

    /**
     * This flag enables the user to use the mouse to select and edit text. To
     * enable this option, use the OR to combine this flag with
     * ENABLE_EXTENDED_FLAGS.
     */
    private static final int ENABLE_WRAP_AT_EOL_OUTPUT = 2;

    /**
     * On windows terminals, this character indicates that a 'special' key has
     * been pressed. This means that a key such as an arrow key, or delete, or
     * home, etc. will be indicated by the next character.
     */
    public static final int SPECIAL_KEY_INDICATOR = 224;

    /**
     * On windows terminals, this character indicates that a special key on the
     * number pad has been pressed.
     */
    public static final int NUMPAD_KEY_INDICATOR = 0;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR,
     * this character indicates an left arrow key press.
     */
    public static final int LEFT_ARROW_KEY = 75;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates an
     * right arrow key press.
     */
    public static final int RIGHT_ARROW_KEY = 77;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates an up
     * arrow key press.
     */
    public static final int UP_ARROW_KEY = 72;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates an
     * down arrow key press.
     */
    public static final int DOWN_ARROW_KEY = 80;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates that
     * the delete key was pressed.
     */
    public static final int DELETE_KEY = 83;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates that
     * the home key was pressed.
     */
    public static final int HOME_KEY = 71;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates that
     * the end key was pressed.
     */
    public static final char END_KEY = 79;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates that
     * the page up key was pressed.
     */
    public static final char PAGE_UP_KEY = 73;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates that
     * the page down key was pressed.
     */
    public static final char PAGE_DOWN_KEY = 81;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR
     * this character indicates that
     * the insert key was pressed.
     */
    public static final char INSERT_KEY = 82;

    /**
     * When following the SPECIAL_KEY_INDICATOR or NUMPAD_KEY_INDICATOR,
     * this character indicates that the escape key was pressed.
     */
    public static final char ESCAPE_KEY = 0;

    private Boolean directConsole;

    private boolean echoEnabled;
    
    String encoding = System.getProperty("jline.WindowsTerminal.input.encoding", System.getProperty("file.encoding"));
    ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding);
    InputStreamReader replayReader;
    
    public WindowsTerminal() {
        String dir = System.getProperty("jline.WindowsTerminal.directConsole");

        if ("true".equals(dir)) {
            directConsole = Boolean.TRUE;
        } else if ("false".equals(dir)) {
            directConsole = Boolean.FALSE;
        }
        
        try {
            replayReader = new InputStreamReader(replayStream, encoding);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        
    }

    private native int getConsoleMode();

    private native void setConsoleMode(final int mode);

    private native int readByte();

    private native int getWindowsTerminalWidth();

    private native int getWindowsTerminalHeight();

    public int readCharacter(final InputStream in) throws IOException {
        // if we can detect that we are directly wrapping the system
        // input, then bypass the input stream and read directly (which
        // allows us to access otherwise unreadable strokes, such as
        // the arrow keys)
        if (directConsole == Boolean.FALSE) {
            return super.readCharacter(in);
        } else if ((directConsole == Boolean.TRUE)
            || ((in == System.in) || (in instanceof FileInputStream
                && (((FileInputStream) in).getFD() == FileDescriptor.in)))) {
            return readByte();
        } else {
            return super.readCharacter(in);
        }
    }

    public void initializeTerminal() throws Exception {
        loadLibrary("jline");

        final int originalMode = getConsoleMode();

        setConsoleMode(originalMode & ~ENABLE_ECHO_INPUT);

        // set the console to raw mode
        int newMode = originalMode
            & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT
                | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT);
        echoEnabled = false;
        setConsoleMode(newMode);

        // at exit, restore the original tty configuration (for JDK 1.3+)
        try {
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void start() {
                    // restore the old console mode
                    setConsoleMode(originalMode);
                }
            });
        } catch (AbstractMethodError ame) {
            // JDK 1.3+ only method. Bummer.
            consumeException(ame);
        }
    }

    private void loadLibrary(final String name) throws IOException {
        // store the DLL in the temporary directory for the System
        String version = WindowsTerminal.class.getPackage().getImplementationVersion();

        if (version == null) {
            version = "";
        }

        version = version.replace('.', '_');

        File f = new File(System.getProperty("java.io.tmpdir"), name + "_"
                + version + ".dll");
        boolean exists = f.isFile(); // check if it already exists

        // extract the embedded jline.dll file from the jar and save
        // it to the current directory
        int bits = 32;

        // check for 64-bit systems and use to appropriate DLL
        if (System.getProperty("os.arch").indexOf("64") != -1)
            bits = 64;

        InputStream in = new BufferedInputStream(WindowsTerminal.class.getResourceAsStream(name + bits + ".dll"));

        OutputStream fout = null;
        try {
            fout = new BufferedOutputStream(
                    new FileOutputStream(f));
            byte[] bytes = new byte[1024 * 10];

            for (int n = 0; n != -1; n = in.read(bytes)) {
                fout.write(bytes, 0, n);
            }

        } catch (IOException ioe) {
            // We might get an IOException trying to overwrite an existing
            // jline.dll file if there is another process using the DLL.
            // If this happens, ignore errors.
            if (!exists) {
                throw ioe;
            }
        } finally {
        	if (fout != null) {
        		try {
        			fout.close();
        		} catch (IOException ioe) {
        			// ignore
        		}
        	}
        }

        // try to clean up the DLL after the JVM exits
        f.deleteOnExit();

        // now actually load the DLL
        System.load(f.getAbsolutePath());
    }

    public int readVirtualKey(InputStream in) throws IOException {
        int indicator = readCharacter(in);

        // in Windows terminals, arrow keys are represented by
        // a sequence of 2 characters. E.g., the up arrow
        // key yields 224, 72
        if (indicator == SPECIAL_KEY_INDICATOR
                || indicator == NUMPAD_KEY_INDICATOR) {
            int key = readCharacter(in);

            switch (key) {
            case UP_ARROW_KEY:
                return CTRL_P; // translate UP -> CTRL-P
            case LEFT_ARROW_KEY:
                return CTRL_B; // translate LEFT -> CTRL-B
            case RIGHT_ARROW_KEY:
                return CTRL_F; // translate RIGHT -> CTRL-F
            case DOWN_ARROW_KEY:
                return CTRL_N; // translate DOWN -> CTRL-N
            case DELETE_KEY:
                return CTRL_QM; // translate DELETE -> CTRL-?
            case HOME_KEY:
                return CTRL_A;
            case END_KEY:
                return CTRL_E;
            case PAGE_UP_KEY:
                return CTRL_K;
            case PAGE_DOWN_KEY:
                return CTRL_L;
            case ESCAPE_KEY:
                return CTRL_OB; // translate ESCAPE -> CTRL-[
            case INSERT_KEY:
                return CTRL_C;
            default:
                return 0;
            }
        } else if (indicator > 128) {
            	// handle unicode characters longer than 2 bytes,
            	// thanks to Marc.Herbert@continuent.com
                replayStream.setInput(indicator, in);
                // replayReader = new InputStreamReader(replayStream, encoding);
                indicator = replayReader.read();
                
        }
        
        return indicator;
        
	}

    public boolean isSupported() {
        return true;
    }

    /**
     * Windows doesn't support ANSI codes by default; disable them.
     */
    public boolean isANSISupported() {
        return false;
    }

    public boolean getEcho() {
        return false;
    }

    /**
     * Unsupported; return the default.
     *
     * @see Terminal#getTerminalWidth
     */
    public int getTerminalWidth() {
        return getWindowsTerminalWidth();
    }

    /**
     * Unsupported; return the default.
     *
     * @see Terminal#getTerminalHeight
     */
    public int getTerminalHeight() {
        return getWindowsTerminalHeight();
    }

    /**
     * No-op for exceptions we want to silently consume.
     */
    private void consumeException(final Throwable e) {
    }

    /**
     * Whether or not to allow the use of the JNI console interaction.
     */
    public void setDirectConsole(Boolean directConsole) {
        this.directConsole = directConsole;
    }

    /**
     * Whether or not to allow the use of the JNI console interaction.
     */
    public Boolean getDirectConsole() {
        return this.directConsole;
    }

    public synchronized boolean isEchoEnabled() {
        return echoEnabled;
    }

    public synchronized void enableEcho() {
        // Must set these four modes at the same time to make it work fine.
        setConsoleMode(getConsoleMode() | ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT
            | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT);
        echoEnabled = true;
    }

    public synchronized void disableEcho() {
        // Must set these four modes at the same time to make it work fine.
        setConsoleMode(getConsoleMode()
            & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT
                | ENABLE_PROCESSED_INPUT | ENABLE_WINDOW_INPUT));
        echoEnabled = true;
    }

    public InputStream getDefaultBindings() {
        return WindowsTerminal.class.getResourceAsStream("windowsbindings.properties");
    }
    
    /**
     * This is awkward and inefficient, but probably the minimal way to add
     * UTF-8 support to JLine
     *
     * @author <a href="mailto:Marc.Herbert@continuent.com">Marc Herbert</a>
     */
    static class ReplayPrefixOneCharInputStream extends InputStream {
        byte firstByte;
        int byteLength;
        InputStream wrappedStream;
        int byteRead;

        final String encoding;
        
        public ReplayPrefixOneCharInputStream(String encoding) {
            this.encoding = encoding;
        }
        
        public void setInput(int recorded, InputStream wrapped) throws IOException {
            this.byteRead = 0;
            this.firstByte = (byte) recorded;
            this.wrappedStream = wrapped;

            byteLength = 1;
            if (encoding.equalsIgnoreCase("UTF-8"))
                setInputUTF8(recorded, wrapped);
            else if (encoding.equalsIgnoreCase("UTF-16"))
                byteLength = 2;
            else if (encoding.equalsIgnoreCase("UTF-32"))
                byteLength = 4;
        }
            
            
        public void setInputUTF8(int recorded, InputStream wrapped) throws IOException {
            // 110yyyyy 10zzzzzz
            if ((firstByte & (byte) 0xE0) == (byte) 0xC0)
                this.byteLength = 2;
            // 1110xxxx 10yyyyyy 10zzzzzz
            else if ((firstByte & (byte) 0xF0) == (byte) 0xE0)
                this.byteLength = 3;
            // 11110www 10xxxxxx 10yyyyyy 10zzzzzz
            else if ((firstByte & (byte) 0xF8) == (byte) 0xF0)
                this.byteLength = 4;
            else
                throw new IOException("invalid UTF-8 first byte: " + firstByte);
        }

        public int read() throws IOException {
            if (available() == 0)
                return -1;

            byteRead++;

            if (byteRead == 1)
                return firstByte;

            return wrappedStream.read();
        }

        /**
        * InputStreamReader is greedy and will try to read bytes in advance. We
        * do NOT want this to happen since we use a temporary/"losing bytes"
        * InputStreamReader above, that's why we hide the real
        * wrappedStream.available() here.
        */
        public int available() {
            return byteLength - byteRead;
        }
    }
    
}