/*
* 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.disableDirectConsole</code> system property
* to <code>true</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 =
getClass().
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(
getClass()
.
getResourceAsStream(
name +
bits + ".dll"));
try {
OutputStream 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);
}
fout.
close();
} 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;
}
}
// 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
getClass().
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;
}
}
}