jline.UnixTerminal Maven / Gradle / Ivy
/*
* 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 java.util.*;
/**
*
* Terminal that is used for unix platforms. Terminal initialization
* is handled by issuing the stty command against the
* /dev/tty file to disable character echoing and enable
* character input. All known unix systems (including
* Linux and Macintosh OS X) support the stty), so this
* implementation should work for an reasonable POSIX system.
*
*
* @author Marc Prud'hommeaux
* @author Updates Dale Kemp 2005-12-03
*/
public class UnixTerminal extends Terminal {
public static final short ARROW_START = 27;
public static final short ARROW_PREFIX = 91;
public static final short ARROW_LEFT = 68;
public static final short ARROW_RIGHT = 67;
public static final short ARROW_UP = 65;
public static final short ARROW_DOWN = 66;
public static final short O_PREFIX = 79;
public static final short HOME_CODE = 72;
public static final short END_CODE = 70;
public static final short DEL_THIRD = 51;
public static final short DEL_SECOND = 126;
private Map terminfo;
private boolean echoEnabled;
private String ttyConfig;
private boolean backspaceDeleteSwitched = false;
private static String sttyCommand =
System.getProperty("jline.sttyCommand", "stty");
String encoding = System.getProperty("input.encoding", "UTF-8");
ReplayPrefixOneCharInputStream replayStream = new ReplayPrefixOneCharInputStream(encoding);
InputStreamReader replayReader;
public UnixTerminal() {
try {
replayReader = new InputStreamReader(replayStream, encoding);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected void checkBackspace(){
String[] ttyConfigSplit = ttyConfig.split(":|=");
if (ttyConfigSplit.length < 7)
return;
if (ttyConfigSplit[6] == null)
return;
backspaceDeleteSwitched = ttyConfigSplit[6].equals("7f");
}
/**
* Remove line-buffered input by invoking "stty -icanon min 1"
* against the current terminal.
*/
public void initializeTerminal() throws IOException, InterruptedException {
// save the initial tty configuration
ttyConfig = stty("-g");
// sanity check
if ((ttyConfig.length() == 0)
|| ((ttyConfig.indexOf("=") == -1)
&& (ttyConfig.indexOf(":") == -1))) {
throw new IOException("Unrecognized stty code: " + ttyConfig);
}
checkBackspace();
// set the console to be character-buffered instead of line-buffered
stty("-icanon min 1");
// disable character echoing
stty("-echo");
echoEnabled = false;
// at exit, restore the original tty configuration (for JDK 1.3+)
try {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void start() {
try {
restoreTerminal();
} catch (Exception e) {
consumeException(e);
}
}
});
} catch (AbstractMethodError ame) {
// JDK 1.3+ only method. Bummer.
consumeException(ame);
}
}
/**
* Restore the original terminal configuration, which can be used when
* shutting down the console reader. The ConsoleReader cannot be
* used after calling this method.
*/
public void restoreTerminal() throws Exception {
if (ttyConfig != null) {
stty(ttyConfig);
ttyConfig = null;
}
resetTerminal();
}
public int readVirtualKey(InputStream in) throws IOException {
int c = readCharacter(in);
if (backspaceDeleteSwitched)
if (c == DELETE)
c = '\b';
else if (c == '\b')
c = DELETE;
// in Unix terminals, arrow keys are represented by
// a sequence of 3 characters. E.g., the up arrow
// key yields 27, 91, 68
if (c == ARROW_START) {
//also the escape key is 27
//thats why we read until we
//have something different than 27
//this is a bugfix, because otherwise
//pressing escape and than an arrow key
//was an undefined state
while (c == ARROW_START)
c = readCharacter(in);
if (c == ARROW_PREFIX || c == O_PREFIX) {
c = readCharacter(in);
if (c == ARROW_UP) {
return CTRL_P;
} else if (c == ARROW_DOWN) {
return CTRL_N;
} else if (c == ARROW_LEFT) {
return CTRL_B;
} else if (c == ARROW_RIGHT) {
return CTRL_F;
} else if (c == HOME_CODE) {
return CTRL_A;
} else if (c == END_CODE) {
return CTRL_E;
} else if (c == DEL_THIRD) {
c = readCharacter(in); // read 4th
return DELETE;
}
}
}
// handle unicode characters, thanks for a patch from [email protected]
if (c > 128) {
// handle unicode characters longer than 2 bytes,
// thanks to [email protected]
replayStream.setInput(c, in);
// replayReader = new InputStreamReader(replayStream, encoding);
c = replayReader.read();
}
return c;
}
/**
* No-op for exceptions we want to silently consume.
*/
private void consumeException(Throwable e) {
}
public boolean isSupported() {
return true;
}
public boolean getEcho() {
return false;
}
/**
* Returns the value of "stty size" width param.
*
* Note: this method caches the value from the
* first time it is called in order to increase speed, which means
* that changing to size of the terminal will not be reflected
* in the console.
*/
public int getTerminalWidth() {
int val = -1;
try {
val = getTerminalProperty("columns");
} catch (Exception e) {
}
if (val == -1) {
val = 80;
}
return val;
}
/**
* Returns the value of "stty size" height param.
*
* Note: this method caches the value from the
* first time it is called in order to increase speed, which means
* that changing to size of the terminal will not be reflected
* in the console.
*/
public int getTerminalHeight() {
int val = -1;
try {
val = getTerminalProperty("rows");
} catch (Exception e) {
}
if (val == -1) {
val = 24;
}
return val;
}
private static int getTerminalProperty(String prop)
throws IOException, InterruptedException {
// need to be able handle both output formats:
// speed 9600 baud; 24 rows; 140 columns;
// and:
// speed 38400 baud; rows = 49; columns = 111; ypixels = 0; xpixels = 0;
String props = stty("-a");
for (StringTokenizer tok = new StringTokenizer(props, ";\n");
tok.hasMoreTokens();) {
String str = tok.nextToken().trim();
if (str.startsWith(prop)) {
int index = str.lastIndexOf(" ");
return Integer.parseInt(str.substring(index).trim());
} else if (str.endsWith(prop)) {
int index = str.indexOf(" ");
return Integer.parseInt(str.substring(0, index).trim());
}
}
return -1;
}
/**
* Execute the stty command with the specified arguments
* against the current active terminal.
*/
private static String stty(final String args)
throws IOException, InterruptedException {
return exec("stty " + args + " < /dev/tty").trim();
}
/**
* Execute the specified command and return the output
* (both stdout and stderr).
*/
private static String exec(final String cmd)
throws IOException, InterruptedException {
return exec(new String[] {
"sh",
"-c",
cmd
});
}
/**
* Execute the specified command and return the output
* (both stdout and stderr).
*/
private static String exec(final String[] cmd)
throws IOException, InterruptedException {
ByteArrayOutputStream bout = new ByteArrayOutputStream();
Process p = Runtime.getRuntime().exec(cmd);
int c;
InputStream in;
in = p.getInputStream();
while ((c = in.read()) != -1) {
bout.write(c);
}
in = p.getErrorStream();
while ((c = in.read()) != -1) {
bout.write(c);
}
p.waitFor();
String result = new String(bout.toByteArray());
return result;
}
/**
* The command to use to set the terminal options. Defaults
* to "stty", or the value of the system property "jline.sttyCommand".
*/
public static void setSttyCommand(String cmd) {
sttyCommand = cmd;
}
/**
* The command to use to set the terminal options. Defaults
* to "stty", or the value of the system property "jline.sttyCommand".
*/
public static String getSttyCommand() {
return sttyCommand;
}
public synchronized boolean isEchoEnabled() {
return echoEnabled;
}
public synchronized void enableEcho() {
try {
stty("echo");
echoEnabled = true;
} catch (Exception e) {
consumeException(e);
}
}
public synchronized void disableEcho() {
try {
stty("-echo");
echoEnabled = false;
} catch (Exception e) {
consumeException(e);
}
}
/**
* This is awkward and inefficient, but probably the minimal way to add
* UTF-8 support to JLine
*
* @author Marc Herbert
*/
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;
}
}
}