* By default, the {@link #wrapInIfNeeded(java.io.InputStream)} 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 jline.WindowsTerminal.directConsole system property
* to false.
*
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.0
*/
public class WindowsTerminal
extends TerminalSupport
{
public static final String DIRECT_CONSOLE = WindowsTerminal.class.getName() + ".directConsole";
public static final String ANSI = WindowsTerminal.class.getName() + ".ansi";
private boolean directConsole;
private int originalMode;
public WindowsTerminal() throws Exception {
super(true);
}
@Override
public void init() throws Exception {
super.init();
setAnsiSupported(Configuration.getBoolean(ANSI, true));
//
// FIXME: Need a way to disable direct console and sysin detection muck
//
setDirectConsole(Configuration.getBoolean(DIRECT_CONSOLE, true));
this.originalMode = getConsoleMode();
setConsoleMode(originalMode & ~ENABLE_ECHO_INPUT.code);
setEchoEnabled(false);
}
/**
* Restore the original terminal configuration, which can be used when
* shutting down the console reader. The ConsoleReader cannot be
* used after calling this method.
*/
@Override
public void restore() throws Exception {
// restore the old console mode
setConsoleMode(originalMode);
super.restore();
}
@Override
public int getWidth() {
int w = getWindowsTerminalWidth();
return w < 1 ? DEFAULT_WIDTH : w;
}
@Override
public int getHeight() {
int h = getWindowsTerminalHeight();
return h < 1 ? DEFAULT_HEIGHT : h;
}
@Override
public void setEchoEnabled(final boolean enabled) {
// Must set these four modes at the same time to make it work fine.
if (enabled) {
setConsoleMode(getConsoleMode() |
ENABLE_ECHO_INPUT.code |
ENABLE_LINE_INPUT.code |
ENABLE_PROCESSED_INPUT.code |
ENABLE_WINDOW_INPUT.code);
}
else {
setConsoleMode(getConsoleMode() &
~(ENABLE_LINE_INPUT.code |
ENABLE_ECHO_INPUT.code |
ENABLE_PROCESSED_INPUT.code |
ENABLE_WINDOW_INPUT.code));
}
super.setEchoEnabled(enabled);
}
/**
* Whether or not to allow the use of the JNI console interaction.
*/
public void setDirectConsole(final boolean flag) {
this.directConsole = flag;
Log.debug("Direct console: ", flag);
}
/**
* Whether or not to allow the use of the JNI console interaction.
*/
public Boolean getDirectConsole() {
return directConsole;
}
@Override
public InputStream wrapInIfNeeded(InputStream in) throws IOException {
if (directConsole && isSystemIn(in)) {
return new InputStream() {
@Override
public int read() throws IOException {
return readByte();
}
};
} else {
return super.wrapInIfNeeded(in);
}
}
protected boolean isSystemIn(final InputStream in) throws IOException {
if (in == null) {
return false;
}
else if (in == System.in) {
return true;
}
else if (in instanceof FileInputStream && ((FileInputStream) in).getFD() == FileDescriptor.in) {
return true;
}
return false;
}
//
// Native Bits
//
private int getConsoleMode() {
return WindowsSupport.getConsoleMode();
}
private void setConsoleMode(int mode) {
WindowsSupport.setConsoleMode(mode);
}
private int readByte() {
return WindowsSupport.readByte();
}
private int getWindowsTerminalWidth() {
return WindowsSupport.getWindowsTerminalWidth();
}
private int getWindowsTerminalHeight() {
return WindowsSupport.getWindowsTerminalHeight();
}
/**
* Console mode
*
* Constants copied wincon.h.
*/
public static enum ConsoleMode
{
/**
* 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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
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.
*/
ENABLE_WRAP_AT_EOL_OUTPUT(2),;
public final int code;
ConsoleMode(final int code) {
this.code = code;
}
}
}
src/main/java/jline/console/ 0000775 0000000 0000000 00000000000 12146220444 0016254 5 ustar 00root root 0000000 0000000 src/main/java/jline/console/ConsoleKeys.java 0000664 0000000 0000000 00000037331 12146220444 0021364 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jline.internal.Log;
/**
* @author Ståle W. Pedersen
*/
public class ConsoleKeys {
private KeyMap keys;
private Map keyMaps;
private Map variables = new HashMap();
public ConsoleKeys(String appName, URL inputrcUrl) {
keyMaps = KeyMap.keyMaps();
loadKeys(appName, inputrcUrl);
}
protected boolean isViEditMode() {
return keys.isViKeyMap();
}
protected boolean setKeyMap (String name) {
KeyMap map = keyMaps.get(name);
if (map == null) {
return false;
}
this.keys = map;
return true;
}
protected Map getKeyMaps() {
return keyMaps;
}
protected KeyMap getKeys() {
return keys;
}
protected void setKeys(KeyMap keys) {
this.keys = keys;
}
protected boolean getViEditMode() {
return keys.isViKeyMap ();
}
protected void loadKeys(String appName, URL inputrcUrl) {
keys = keyMaps.get(KeyMap.EMACS);
try {
InputStream input = inputrcUrl.openStream();
try {
loadKeys(input, appName);
Log.debug("Loaded user configuration: ", inputrcUrl);
}
finally {
try {
input.close();
} catch (IOException e) {
// Ignore
}
}
}
catch (IOException e) {
if (inputrcUrl.getProtocol().equals("file")) {
File file = new File(inputrcUrl.getPath());
if (file.exists()) {
Log.warn("Unable to read user configuration: ", inputrcUrl, e);
}
} else {
Log.warn("Unable to read user configuration: ", inputrcUrl, e);
}
}
}
private void loadKeys(InputStream input, String appName) throws IOException {
BufferedReader reader = new BufferedReader( new java.io.InputStreamReader( input ) );
String line;
boolean parsing = true;
List ifsStack = new ArrayList();
while ( (line = reader.readLine()) != null ) {
try {
line = line.trim();
if (line.length() == 0) {
continue;
}
if (line.charAt(0) == '#') {
continue;
}
int i = 0;
if (line.charAt(i) == '$') {
String cmd;
String args;
for (++i; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
int s = i;
for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++);
cmd = line.substring(s, i);
for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
s = i;
for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++);
args = line.substring(s, i);
if ("if".equalsIgnoreCase(cmd)) {
ifsStack.add( parsing );
if (!parsing) {
continue;
}
if (args.startsWith("term=")) {
// TODO
} else if (args.startsWith("mode=")) {
if (args.equalsIgnoreCase("mode=vi")) {
parsing = isViEditMode();
} else if (args.equals("mode=emacs")) {
parsing = !isViEditMode();
} else {
parsing = false;
}
} else {
parsing = args.equalsIgnoreCase(appName);
}
} else if ("else".equalsIgnoreCase(cmd)) {
if (ifsStack.isEmpty()) {
throw new IllegalArgumentException("$else found without matching $if");
}
boolean invert = true;
for (boolean b : ifsStack) {
if (!b) {
invert = false;
break;
}
}
if (invert) {
parsing = !parsing;
}
} else if ("endif".equalsIgnoreCase(cmd)) {
if (ifsStack.isEmpty()) {
throw new IllegalArgumentException("endif found without matching $if");
}
parsing = ifsStack.remove( ifsStack.size() - 1 );
} else if ("include".equalsIgnoreCase(cmd)) {
// TODO
}
continue;
}
if (!parsing) {
continue;
}
boolean equivalency;
String keySeq = "";
if (line.charAt(i++) == '"') {
boolean esc = false;
for (;; i++) {
if (i >= line.length()) {
throw new IllegalArgumentException("Missing closing quote on line '" + line + "'");
}
if (esc) {
esc = false;
} else if (line.charAt(i) == '\\') {
esc = true;
} else if (line.charAt(i) == '"') {
break;
}
}
}
for (; i < line.length() && line.charAt(i) != ':'
&& line.charAt(i) != ' ' && line.charAt(i) != '\t'
; i++);
keySeq = line.substring(0, i);
equivalency = (i + 1 < line.length() && line.charAt(i) == ':' && line.charAt(i + 1) == '=');
i++;
if (equivalency) {
i++;
}
if (keySeq.equalsIgnoreCase("set")) {
String key;
String val;
for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
int s = i;
for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++);
key = line.substring( s, i );
for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
s = i;
for (; i < line.length() && (line.charAt(i) != ' ' && line.charAt(i) != '\t'); i++);
val = line.substring( s, i );
setVar( key, val );
} else {
for (; i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t'); i++);
int start = i;
if (i < line.length() && (line.charAt(i) == '\'' || line.charAt(i) == '\"')) {
char delim = line.charAt(i++);
boolean esc = false;
for (;; i++) {
if (i >= line.length()) {
break;
}
if (esc) {
esc = false;
} else if (line.charAt(i) == '\\') {
esc = true;
} else if (line.charAt(i) == delim) {
break;
}
}
}
for (; i < line.length() && line.charAt(i) != ' ' && line.charAt(i) != '\t'; i++);
String val = line.substring(Math.min(start, line.length()), Math.min(i, line.length()));
if (keySeq.charAt(0) == '"') {
keySeq = translateQuoted(keySeq);
} else {
// Bind key name
String keyName = keySeq.lastIndexOf('-') > 0 ? keySeq.substring( keySeq.lastIndexOf('-') + 1 ) : keySeq;
char key = getKeyFromName(keyName);
keyName = keySeq.toLowerCase();
keySeq = "";
if (keyName.contains("meta-") || keyName.contains("m-")) {
keySeq += "\u001b";
}
if (keyName.contains("control-") || keyName.contains("c-") || keyName.contains("ctrl-")) {
key = (char)(Character.toUpperCase( key ) & 0x1f);
}
keySeq += key;
}
if (val.length() > 0 && (val.charAt(0) == '\'' || val.charAt(0) == '\"')) {
keys.bind( keySeq, translateQuoted(val) );
} else {
String operationName = val.replace('-', '_').toUpperCase();
try {
keys.bind(keySeq, Operation.valueOf(operationName));
} catch(IllegalArgumentException e) {
Log.info("Unable to bind key for unsupported operation: ", val);
}
}
}
} catch (IllegalArgumentException e) {
Log.warn("Unable to parse user configuration: ", e);
}
}
}
private String translateQuoted(String keySeq) {
int i;
String str = keySeq.substring( 1, keySeq.length() - 1 );
keySeq = "";
for (i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == '\\') {
boolean ctrl = str.regionMatches(i, "\\C-", 0, 3)|| str.regionMatches(i, "\\M-\\C-", 0, 6);
boolean meta = str.regionMatches(i, "\\M-", 0, 3)|| str.regionMatches(i, "\\C-\\M-", 0, 6);
i += (meta ? 3 : 0) + (ctrl ? 3 : 0) + (!meta && !ctrl ? 1 : 0);
if (i >= str.length()) {
break;
}
c = str.charAt(i);
if (meta) {
keySeq += "\u001b";
}
if (ctrl) {
c = c == '?' ? 0x7f : (char)(Character.toUpperCase( c ) & 0x1f);
}
if (!meta && !ctrl) {
switch (c) {
case 'a': c = 0x07; break;
case 'b': c = '\b'; break;
case 'd': c = 0x7f; break;
case 'e': c = 0x1b; break;
case 'f': c = '\f'; break;
case 'n': c = '\n'; break;
case 'r': c = '\r'; break;
case 't': c = '\t'; break;
case 'v': c = 0x0b; break;
case '\\': c = '\\'; break;
case '0': case '1': case '2': case '3':
case '4': case '5': case '6': case '7':
c = 0;
for (int j = 0; j < 3; j++, i++) {
if (i >= str.length()) {
break;
}
int k = Character.digit(str.charAt(i), 8);
if (k < 0) {
break;
}
c = (char)(c * 8 + k);
}
c &= 0xFF;
break;
case 'x':
i++;
c = 0;
for (int j = 0; j < 2; j++, i++) {
if (i >= str.length()) {
break;
}
int k = Character.digit(str.charAt(i), 16);
if (k < 0) {
break;
}
c = (char)(c * 16 + k);
}
c &= 0xFF;
break;
case 'u':
i++;
c = 0;
for (int j = 0; j < 4; j++, i++) {
if (i >= str.length()) {
break;
}
int k = Character.digit(str.charAt(i), 16);
if (k < 0) {
break;
}
c = (char)(c * 16 + k);
}
break;
}
}
keySeq += c;
} else {
keySeq += c;
}
}
return keySeq;
}
private char getKeyFromName(String name) {
if ("DEL".equalsIgnoreCase(name) || "Rubout".equalsIgnoreCase(name)) {
return 0x7f;
} else if ("ESC".equalsIgnoreCase(name) || "Escape".equalsIgnoreCase(name)) {
return '\033';
} else if ("LFD".equalsIgnoreCase(name) || "NewLine".equalsIgnoreCase(name)) {
return '\n';
} else if ("RET".equalsIgnoreCase(name) || "Return".equalsIgnoreCase(name)) {
return '\r';
} else if ("SPC".equalsIgnoreCase(name) || "Space".equalsIgnoreCase(name)) {
return ' ';
} else if ("Tab".equalsIgnoreCase(name)) {
return '\t';
} else {
return name.charAt(0);
}
}
private void setVar(String key, String val) {
if ("keymap".equalsIgnoreCase(key)) {
if (keyMaps.containsKey(val)) {
keys = keyMaps.get(val);
}
} else if ("editing-mode".equals(key)) {
if ("vi".equalsIgnoreCase(val)) {
keys = keyMaps.get(KeyMap.VI_INSERT);
} else if ("emacs".equalsIgnoreCase(key)) {
keys = keyMaps.get(KeyMap.EMACS);
}
} else if ("blink-matching-paren".equals(key)) {
if ("on".equalsIgnoreCase(val)) {
keys.setBlinkMatchingParen(true);
} else if ("off".equalsIgnoreCase(val)) {
keys.setBlinkMatchingParen(false);
}
}
/*
* Technically variables should be defined as a functor class
* so that validation on the variable value can be done at parse
* time. This is a stop-gap.
*/
variables.put(key, val);
}
/**
* Retrieves the value of a variable that was set in the .inputrc file
* during processing
* @param var The variable name
* @return The variable value.
*/
public String getVariable(String var) {
return variables.get (var);
}
}
src/main/java/jline/console/ConsoleReader.java 0000664 0000000 0000000 00000370542 12146220444 0021657 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Stack;
import jline.Terminal;
import jline.TerminalFactory;
import jline.UnixTerminal;
import jline.console.completer.CandidateListCompletionHandler;
import jline.console.completer.Completer;
import jline.console.completer.CompletionHandler;
import jline.console.history.History;
import jline.console.history.MemoryHistory;
import jline.internal.Configuration;
import jline.internal.InputStreamReader;
import jline.internal.Log;
import jline.internal.NonBlockingInputStream;
import jline.internal.Nullable;
import jline.internal.Urls;
import org.fusesource.jansi.AnsiOutputStream;
import static jline.internal.Preconditions.checkNotNull;
/**
* A reader for console applications. It supports custom tab-completion,
* saveable command history, and command line editing. On some platforms,
* platform-specific commands will need to be issued before the reader will
* function properly. See {@link jline.Terminal#init} for convenience
* methods for issuing platform-specific setup commands.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @author Guillaume Nodet
*/
public class ConsoleReader
{
public static final String JLINE_NOBELL = "jline.nobell";
public static final String JLINE_ESC_TIMEOUT = "jline.esc.timeout";
public static final String JLINE_INPUTRC = "jline.inputrc";
public static final String INPUT_RC = ".inputrc";
public static final String DEFAULT_INPUT_RC = "/etc/inputrc";
public static final char BACKSPACE = '\b';
public static final char RESET_LINE = '\r';
public static final char KEYBOARD_BELL = '\07';
public static final char NULL_MASK = 0;
public static final int TAB_WIDTH = 4;
private static final ResourceBundle
resources = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName());
private final Terminal terminal;
private final Writer out;
private final CursorBuffer buf = new CursorBuffer();
private String prompt;
private int promptLen;
private boolean expandEvents = true;
private boolean bellEnabled = !Configuration.getBoolean(JLINE_NOBELL, true);
private boolean handleUserInterrupt = false;
private Character mask;
private Character echoCharacter;
private StringBuffer searchTerm = null;
private String previousSearchTerm = "";
private int searchIndex = -1;
private int parenBlinkTimeout = 500;
/*
* The reader and the nonBlockingInput go hand-in-hand. The reader wraps
* the nonBlockingInput, but we have to retain a handle to it so that
* we can shut down its blocking read thread when we go away.
*/
private NonBlockingInputStream in;
private long escapeTimeout;
private Reader reader;
/*
* TODO: Please read the comments about this in setInput(), but this needs
* to be done away with.
*/
private boolean isUnitTestInput;
/**
* Last character searched for with a vi character search
*/
private char charSearchChar = 0; // Character to search for
private char charSearchLastInvokeChar = 0; // Most recent invocation key
private char charSearchFirstInvokeChar = 0;// First character that invoked
/**
* The vi yank buffer
*/
private String yankBuffer = "";
private String encoding;
private boolean recording;
private String macro = "";
private String appName;
private URL inputrcUrl;
private ConsoleKeys consoleKeys;
private String commentBegin = null;
private boolean skipLF = false;
/**
* Set to true if the reader should attempt to detect copy-n-paste. The
* effect of this that an attempt is made to detect if tab is quickly
* followed by another character, then it is assumed that the tab was
* a literal tab as part of a copy-and-paste operation and is inserted as
* such.
*/
private boolean copyPasteDetection = false;
/*
* Current internal state of the line reader
*/
private State state = State.NORMAL;
/**
* Possible states in which the current readline operation may be in.
*/
private static enum State {
/**
* The user is just typing away
*/
NORMAL,
/**
* In the middle of a emacs seach
*/
SEARCH,
FORWARD_SEARCH,
/**
* VI "yank-to" operation ("y" during move mode)
*/
VI_YANK_TO,
/**
* VI "delete-to" operation ("d" during move mode)
*/
VI_DELETE_TO,
/**
* VI "change-to" operation ("c" during move mode)
*/
VI_CHANGE_TO
}
public ConsoleReader() throws IOException {
this(null, new FileInputStream(FileDescriptor.in), System.out, null);
}
public ConsoleReader(final InputStream in, final OutputStream out) throws IOException {
this(null, in, out, null);
}
public ConsoleReader(final InputStream in, final OutputStream out, final Terminal term) throws IOException {
this(null, in, out, term);
}
public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term) throws IOException {
this(appName, in, out, term, null);
}
public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term, final @Nullable String encoding)
throws IOException
{
this.appName = appName != null ? appName : "JLine";
this.encoding = encoding != null ? encoding : Configuration.getEncoding();
this.terminal = term != null ? term : TerminalFactory.get();
this.out = new OutputStreamWriter(terminal.wrapOutIfNeeded(out), this.encoding);
setInput( in );
this.inputrcUrl = getInputRc();
consoleKeys = new ConsoleKeys(appName, inputrcUrl);
}
private URL getInputRc() throws IOException {
String path = Configuration.getString(JLINE_INPUTRC);
if (path == null) {
File f = new File(Configuration.getUserHome(), INPUT_RC);
if (!f.exists()) {
f = new File(DEFAULT_INPUT_RC);
}
return f.toURI().toURL();
} else {
return Urls.create(path);
}
}
public KeyMap getKeys() {
return consoleKeys.getKeys();
}
void setInput(final InputStream in) throws IOException {
this.escapeTimeout = Configuration.getLong(JLINE_ESC_TIMEOUT, 100);
/*
* This is gross and here is how to fix it. In getCurrentPosition()
* and getCurrentAnsiRow(), the logic is disabled when running unit
* tests and the fact that it is a unit test is determined by knowing
* if the original input stream was a ByteArrayInputStream. So, this
* is our test to do this. What SHOULD happen is that the unit
* tests should pass in a terminal that is appropriately configured
* such that whatever behavior they expect to happen (or not happen)
* happens (or doesn't).
*
* So, TODO, get rid of this and fix the unit tests.
*/
this.isUnitTestInput = in instanceof ByteArrayInputStream;
boolean nonBlockingEnabled =
escapeTimeout > 0L
&& terminal.isSupported()
&& in != null;
/*
* If we had a non-blocking thread already going, then shut it down
* and start a new one.
*/
if (this.in != null) {
this.in.shutdown();
}
final InputStream wrapped = terminal.wrapInIfNeeded( in );
this.in = new NonBlockingInputStream(wrapped, nonBlockingEnabled);
this.reader = new InputStreamReader( this.in, encoding );
}
/**
* Shuts the console reader down. This method should be called when you
* have completed using the reader as it shuts down and cleans up resources
* that would otherwise be "leaked".
*/
public void shutdown() {
if (in != null) {
in.shutdown();
}
}
/**
* Shuts down the ConsoleReader if the JVM attempts to clean it up.
*/
@Override
protected void finalize() throws Throwable {
try {
shutdown();
}
finally {
super.finalize();
}
}
public InputStream getInput() {
return in;
}
public Writer getOutput() {
return out;
}
public Terminal getTerminal() {
return terminal;
}
public CursorBuffer getCursorBuffer() {
return buf;
}
public void setExpandEvents(final boolean expand) {
this.expandEvents = expand;
}
public boolean getExpandEvents() {
return expandEvents;
}
/**
* Enables or disables copy and paste detection. The effect of enabling this
* this setting is that when a tab is received immediately followed by another
* character, the tab will not be treated as a completion, but as a tab literal.
* @param onoff true if detection is enabled
*/
public void setCopyPasteDetection(final boolean onoff) {
copyPasteDetection = onoff;
}
/**
* @return true if copy and paste detection is enabled.
*/
public boolean isCopyPasteDetectionEnabled() {
return copyPasteDetection;
}
/**
* Set whether the console bell is enabled.
*
* @param enabled true if enabled; false otherwise
* @since 2.7
*/
public void setBellEnabled(boolean enabled) {
this.bellEnabled = enabled;
}
/**
* Get whether the console bell is enabled
*
* @return true if enabled; false otherwise
* @since 2.7
*/
public boolean getBellEnabled() {
return bellEnabled;
}
/**
* Set whether user interrupts (ctrl-C) are handled by having JLine
* throw {@link UserInterruptException} from {@link #readLine}.
* Otherwise, the JVM will handle {@code SIGINT} as normal, which
* usually causes it to exit. The default is {@code false}.
*
* @since 2.10
*/
public void setHandleUserInterrupt(boolean enabled)
{
this.handleUserInterrupt = enabled;
}
/**
* Get whether user interrupt handling is enabled
*
* @return true if enabled; false otherwise
* @since 2.10
*/
public boolean getHandleUserInterrupt()
{
return handleUserInterrupt;
}
/**
* Sets the string that will be used to start a comment when the
* insert-comment key is struck.
* @param commentBegin The begin comment string.
* @since 2.7
*/
public void setCommentBegin(String commentBegin) {
this.commentBegin = commentBegin;
}
/**
* @return the string that will be used to start a comment when the
* insert-comment key is struck.
* @since 2.7
*/
public String getCommentBegin() {
String str = commentBegin;
if (str == null) {
str = consoleKeys.getVariable("comment-begin");
if (str == null) {
str = "#";
}
}
return str;
}
public void setPrompt(final String prompt) {
this.prompt = prompt;
this.promptLen = ((prompt == null) ? 0 : stripAnsi(lastLine(prompt)).length());
}
public String getPrompt() {
return prompt;
}
/**
* Set the echo character. For example, to have "*" entered when a password is typed:
*
*
*
* will restore normal character echoing. Setting the character to
*
*
* new Character(0)
*
*
* will cause nothing to be echoed.
*
* @param c the character to echo to the console in place of the typed character.
*/
public void setEchoCharacter(final Character c) {
this.echoCharacter = c;
}
/**
* Returns the echo character.
*/
public Character getEchoCharacter() {
return echoCharacter;
}
/**
* Erase the current line.
*
* @return false if we failed (e.g., the buffer was empty)
*/
protected final boolean resetLine() throws IOException {
if (buf.cursor == 0) {
return false;
}
backspaceAll();
return true;
}
int getCursorPosition() {
// FIXME: does not handle anything but a line with a prompt absolute position
return promptLen + buf.cursor;
}
/**
* Returns the text after the last '\n'.
* prompt is returned if no '\n' characters are present.
* null is returned if prompt is null.
*/
private String lastLine(String str) {
if (str == null) return "";
int last = str.lastIndexOf("\n");
if (last >= 0) {
return str.substring(last + 1, str.length());
}
return str;
}
private String stripAnsi(String str) {
if (str == null) return "";
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
AnsiOutputStream aos = new AnsiOutputStream(baos);
aos.write(str.getBytes());
aos.flush();
return baos.toString();
} catch (IOException e) {
return str;
}
}
/**
* Move the cursor position to the specified absolute index.
*/
public final boolean setCursorPosition(final int position) throws IOException {
if (position == buf.cursor) {
return true;
}
return moveCursor(position - buf.cursor) != 0;
}
/**
* Set the current buffer's content to the specified {@link String}. The
* visual console will be modified to show the current buffer.
*
* @param buffer the new contents of the buffer.
*/
private void setBuffer(final String buffer) throws IOException {
// don't bother modifying it if it is unchanged
if (buffer.equals(buf.buffer.toString())) {
return;
}
// obtain the difference between the current buffer and the new one
int sameIndex = 0;
for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1)
&& (i < l2); i++) {
if (buffer.charAt(i) == buf.buffer.charAt(i)) {
sameIndex++;
}
else {
break;
}
}
int diff = buf.cursor - sameIndex;
if (diff < 0) { // we can't backspace here so try from the end of the buffer
moveToEnd();
diff = buf.buffer.length() - sameIndex;
}
backspace(diff); // go back for the differences
killLine(); // clear to the end of the line
buf.buffer.setLength(sameIndex); // the new length
putString(buffer.substring(sameIndex)); // append the differences
}
private void setBuffer(final CharSequence buffer) throws IOException {
setBuffer(String.valueOf(buffer));
}
/**
* Output put the prompt + the current buffer
*/
public final void drawLine() throws IOException {
String prompt = getPrompt();
if (prompt != null) {
print(prompt);
}
print(buf.buffer.toString());
if (buf.length() != buf.cursor) { // not at end of line
back(buf.length() - buf.cursor - 1);
}
// force drawBuffer to check for weird wrap (after clear screen)
drawBuffer();
}
/**
* Clear the line and redraw it.
*/
public final void redrawLine() throws IOException {
print(RESET_LINE);
// flush();
drawLine();
}
/**
* Clear the buffer and add its contents to the history.
*
* @return the former contents of the buffer.
*/
final String finishBuffer() throws IOException { // FIXME: Package protected because used by tests
String str = buf.buffer.toString();
String historyLine = str;
if (expandEvents) {
str = expandEvents(str);
// all post-expansion occurrences of '!' must have been escaped, so re-add escape to each
historyLine = str.replace("!", "\\!");
// only leading '^' results in expansion, so only re-add escape for that case
historyLine = historyLine.replaceAll("^\\^", "\\\\^");
}
// we only add it to the history if the buffer is not empty
// and if mask is null, since having a mask typically means
// the string was a password. We clear the mask after this call
if (str.length() > 0) {
if (mask == null && isHistoryEnabled()) {
history.add(historyLine);
}
else {
mask = null;
}
}
history.moveToEnd();
buf.buffer.setLength(0);
buf.cursor = 0;
return str;
}
/**
* Expand event designator such as !!, !#, !3, etc...
* See http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html
*/
protected String expandEvents(String str) throws IOException {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
switch (c) {
case '\\':
// any '\!' should be considered an expansion escape, so skip expansion and strip the escape character
// a leading '\^' should be considered an expansion escape, so skip expansion and strip the escape character
// otherwise, add the escape
if (i + 1 < str.length()) {
char nextChar = str.charAt(i+1);
if (nextChar == '!' || (nextChar == '^' && i == 0)) {
c = nextChar;
i++;
}
}
sb.append(c);
break;
case '!':
if (i + 1 < str.length()) {
c = str.charAt(++i);
boolean neg = false;
String rep = null;
int i1, idx;
switch (c) {
case '!':
if (history.size() == 0) {
throw new IllegalArgumentException("!!: event not found");
}
rep = history.get(history.index() - 1).toString();
break;
case '#':
sb.append(sb.toString());
break;
case '?':
i1 = str.indexOf('?', i + 1);
if (i1 < 0) {
i1 = str.length();
}
String sc = str.substring(i + 1, i1);
i = i1;
idx = searchBackwards(sc);
if (idx < 0) {
throw new IllegalArgumentException("!?" + sc + ": event not found");
} else {
rep = history.get(idx).toString();
}
break;
case ' ':
case '\t':
sb.append('!');
sb.append(c);
break;
case '-':
neg = true;
i++;
// fall through
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
i1 = i;
for (; i < str.length(); i++) {
c = str.charAt(i);
if (c < '0' || c > '9') {
break;
}
}
idx = 0;
try {
idx = Integer.parseInt(str.substring(i1, i));
} catch (NumberFormatException e) {
throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found");
}
if (neg) {
if (idx > 0 && idx <= history.size()) {
rep = (history.get(history.index() - idx)).toString();
} else {
throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found");
}
} else {
if (idx > history.index() - history.size() && idx <= history.index()) {
rep = (history.get(idx - 1)).toString();
} else {
throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found");
}
}
break;
default:
String ss = str.substring(i);
i = str.length();
idx = searchBackwards(ss, history.index(), true);
if (idx < 0) {
throw new IllegalArgumentException("!" + ss + ": event not found");
} else {
rep = history.get(idx).toString();
}
break;
}
if (rep != null) {
sb.append(rep);
}
} else {
sb.append(c);
}
break;
case '^':
if (i == 0) {
int i1 = str.indexOf('^', i + 1);
int i2 = str.indexOf('^', i1 + 1);
if (i2 < 0) {
i2 = str.length();
}
if (i1 > 0 && i2 > 0) {
String s1 = str.substring(i + 1, i1);
String s2 = str.substring(i1 + 1, i2);
String s = history.get(history.index() - 1).toString().replace(s1, s2);
sb.append(s);
i = i2 + 1;
break;
}
}
sb.append(c);
break;
default:
sb.append(c);
break;
}
}
String result = sb.toString();
if (!str.equals(result)) {
print(result);
println();
flush();
}
return result;
}
/**
* Write out the specified string to the buffer and the output stream.
*/
public final void putString(final CharSequence str) throws IOException {
buf.write(str);
if (mask == null) {
// no masking
print(str);
} else if (mask == NULL_MASK) {
// don't print anything
} else {
print(mask, str.length());
}
drawBuffer();
}
/**
* Redraw the rest of the buffer from the cursor onwards. This is necessary
* for inserting text into the buffer.
*
* @param clear the number of characters to clear after the end of the buffer
*/
private void drawBuffer(final int clear) throws IOException {
// debug ("drawBuffer: " + clear);
if (buf.cursor == buf.length() && clear == 0) {
} else {
char[] chars = buf.buffer.substring(buf.cursor).toCharArray();
if (mask != null) {
Arrays.fill(chars, mask);
}
if (terminal.hasWeirdWrap()) {
// need to determine if wrapping will occur:
int width = terminal.getWidth();
int pos = getCursorPosition();
for (int i = 0; i < chars.length; i++) {
print(chars[i]);
if ((pos + i + 1) % width == 0) {
print(32); // move cursor to next line by printing dummy space
print(13); // CR / not newline.
}
}
} else {
print(chars);
}
clearAhead(clear, chars.length);
if (terminal.isAnsiSupported()) {
if (chars.length > 0) {
back(chars.length);
}
} else {
back(chars.length);
}
}
if (terminal.hasWeirdWrap()) {
int width = terminal.getWidth();
// best guess on whether the cursor is in that weird location...
// Need to do this without calling ansi cursor location methods
// otherwise it breaks paste of wrapped lines in xterm.
if (getCursorPosition() > 0 && (getCursorPosition() % width == 0)
&& buf.cursor == buf.length() && clear == 0) {
// the following workaround is reverse-engineered from looking
// at what bash sent to the terminal in the same situation
print(32); // move cursor to next line by printing dummy space
print(13); // CR / not newline.
}
}
}
/**
* Redraw the rest of the buffer from the cursor onwards. This is necessary
* for inserting text into the buffer.
*/
private void drawBuffer() throws IOException {
drawBuffer(0);
}
/**
* Clear ahead the specified number of characters without moving the cursor.
*
* @param num the number of characters to clear
* @param delta the difference between the internal cursor and the screen
* cursor - if > 0, assume some stuff was printed and weird wrap has to be
* checked
*/
private void clearAhead(final int num, int delta) throws IOException {
if (num == 0) {
return;
}
if (terminal.isAnsiSupported()) {
int width = terminal.getWidth();
int screenCursorCol = getCursorPosition() + delta;
// clear current line
printAnsiSequence("K");
// if cursor+num wraps, then we need to clear the line(s) below too
int curCol = screenCursorCol % width;
int endCol = (screenCursorCol + num - 1) % width;
int lines = num / width;
if (endCol < curCol) lines++;
for (int i = 0; i < lines; i++) {
printAnsiSequence("B");
printAnsiSequence("2K");
}
for (int i = 0; i < lines; i++) {
printAnsiSequence("A");
}
return;
}
// print blank extra characters
print(' ', num);
// we need to flush here so a "clever" console doesn't just ignore the redundancy
// of a space followed by a backspace.
// flush();
// reset the visual cursor
back(num);
// flush();
}
/**
* Move the visual cursor backwards without modifying the buffer cursor.
*/
protected void back(final int num) throws IOException {
if (num == 0) return;
if (terminal.isAnsiSupported()) {
int width = getTerminal().getWidth();
int cursor = getCursorPosition();
int realCursor = cursor + num;
int realCol = realCursor % width;
int newCol = cursor % width;
int moveup = num / width;
int delta = realCol - newCol;
if (delta < 0) moveup++;
if (moveup > 0) {
printAnsiSequence(moveup + "A");
}
printAnsiSequence((1 + newCol) + "G");
return;
}
print(BACKSPACE, num);
// flush();
}
/**
* Flush the console output stream. This is important for printout out single characters (like a backspace or
* keyboard) that we want the console to handle immediately.
*/
public void flush() throws IOException {
out.flush();
}
private int backspaceAll() throws IOException {
return backspace(Integer.MAX_VALUE);
}
/**
* Issue num backspaces.
*
* @return the number of characters backed up
*/
private int backspace(final int num) throws IOException {
if (buf.cursor == 0) {
return 0;
}
int count = 0;
int termwidth = getTerminal().getWidth();
int lines = getCursorPosition() / termwidth;
count = moveCursor(-1 * num) * -1;
buf.buffer.delete(buf.cursor, buf.cursor + count);
if (getCursorPosition() / termwidth != lines) {
if (terminal.isAnsiSupported()) {
// debug("doing backspace redraw: " + getCursorPosition() + " on " + termwidth + ": " + lines);
printAnsiSequence("K");
// if cursor+num wraps, then we need to clear the line(s) below too
// last char printed is one pos less than cursor so we subtract
// one
/*
// TODO: fixme (does not work - test with reverse search with wrapping line and CTRL-E)
int endCol = (getCursorPosition() + num - 1) % termwidth;
int curCol = getCursorPosition() % termwidth;
if (endCol < curCol) lines++;
for (int i = 1; i < lines; i++) {
printAnsiSequence("B");
printAnsiSequence("2K");
}
for (int i = 1; i < lines; i++) {
printAnsiSequence("A");
}
return count;
*/
}
}
drawBuffer(count);
return count;
}
/**
* Issue a backspace.
*
* @return true if successful
*/
public boolean backspace() throws IOException {
return backspace(1) == 1;
}
protected boolean moveToEnd() throws IOException {
if (buf.cursor == buf.length()) {
return true;
}
return moveCursor(buf.length() - buf.cursor) > 0;
}
/**
* Delete the character at the current position and redraw the remainder of the buffer.
*/
private boolean deleteCurrentCharacter() throws IOException {
if (buf.length() == 0 || buf.cursor == buf.length()) {
return false;
}
buf.buffer.deleteCharAt(buf.cursor);
drawBuffer(1);
return true;
}
/**
* This method is calling while doing a delete-to ("d"), change-to ("c"),
* or yank-to ("y") and it filters out only those movement operations
* that are allowable during those operations. Any operation that isn't
* allow drops you back into movement mode.
*
* @param op The incoming operation to remap
* @return The remaped operation
*/
private Operation viDeleteChangeYankToRemap (Operation op) {
switch (op) {
case VI_EOF_MAYBE:
case ABORT:
case BACKWARD_CHAR:
case FORWARD_CHAR:
case END_OF_LINE:
case VI_MATCH:
case VI_BEGNNING_OF_LINE_OR_ARG_DIGIT:
case VI_ARG_DIGIT:
case VI_PREV_WORD:
case VI_END_WORD:
case VI_CHAR_SEARCH:
case VI_NEXT_WORD:
case VI_FIRST_PRINT:
case VI_GOTO_MARK:
case VI_COLUMN:
case VI_DELETE_TO:
case VI_YANK_TO:
case VI_CHANGE_TO:
return op;
default:
return Operation.VI_MOVEMENT_MODE;
}
}
/**
* Deletes the previous character from the cursor position
* @param count number of times to do it.
* @return true if it was done.
* @throws IOException
*/
private boolean viRubout(int count) throws IOException {
boolean ok = true;
for (int i = 0; ok && i < count; i++) {
ok = backspace();
}
return ok;
}
/**
* Deletes the character you are sitting on and sucks the rest of
* the line in from the right.
* @param count Number of times to perform the operation.
* @return true if its works, false if it didn't
* @throws IOException
*/
private boolean viDelete(int count) throws IOException {
boolean ok = true;
for (int i = 0; ok && i < count; i++) {
ok = deleteCurrentCharacter();
}
return ok;
}
/**
* Switches the case of the current character from upper to lower
* or lower to upper as necessary and advances the cursor one
* position to the right.
* @param count The number of times to repeat
* @return true if it completed successfully, false if not all
* case changes could be completed.
* @throws IOException
*/
private boolean viChangeCase(int count) throws IOException {
boolean ok = true;
for (int i = 0; ok && i < count; i++) {
ok = buf.cursor < buf.buffer.length ();
if (ok) {
char ch = buf.buffer.charAt(buf.cursor);
if (Character.isUpperCase(ch)) {
ch = Character.toLowerCase(ch);
}
else if (Character.isLowerCase(ch)) {
ch = Character.toUpperCase(ch);
}
buf.buffer.setCharAt(buf.cursor, ch);
drawBuffer(1);
moveCursor(1);
}
}
return ok;
}
/**
* Implements the vi change character command (in move-mode "r"
* followed by the character to change to).
* @param count Number of times to perform the action
* @param c The character to change to
* @return Whether or not there were problems encountered
* @throws IOException
*/
private boolean viChangeChar(int count, int c) throws IOException {
// EOF, ESC, or CTRL-C aborts.
if (c < 0 || c == '\033' || c == '\003') {
return true;
}
boolean ok = true;
for (int i = 0; ok && i < count; i++) {
ok = buf.cursor < buf.buffer.length ();
if (ok) {
buf.buffer.setCharAt(buf.cursor, (char) c);
drawBuffer(1);
if (i < (count-1)) {
moveCursor(1);
}
}
}
return ok;
}
/**
* This is a close facsimile of the actual vi previous word logic. In
* actual vi words are determined by boundaries of identity characterse.
* This logic is a bit more simple and simply looks at white space or
* digits or characters. It should be revised at some point.
*
* @param count number of iterations
* @return true if the move was successful, false otherwise
* @throws IOException
*/
private boolean viPreviousWord(int count) throws IOException {
boolean ok = true;
if (buf.cursor == 0) {
return false;
}
int pos = buf.cursor - 1;
for (int i = 0; pos > 0 && i < count; i++) {
// If we are on white space, then move back.
while (pos > 0 && isWhitespace(buf.buffer.charAt(pos))) {
--pos;
}
while (pos > 0 && !isDelimiter(buf.buffer.charAt(pos-1))) {
--pos;
}
if (pos > 0 && i < (count-1)) {
--pos;
}
}
setCursorPosition(pos);
return ok;
}
/**
* Performs the vi "delete-to" action, deleting characters between a given
* span of the input line.
* @param startPos The start position
* @param endPos The end position.
* @return true if it succeeded, false otherwise
* @throws IOException
*/
private boolean viDeleteTo(int startPos, int endPos) throws IOException {
if (startPos == endPos) {
return true;
}
if (endPos < startPos) {
int tmp = endPos;
endPos = startPos;
startPos = tmp;
}
setCursorPosition(startPos);
buf.cursor = startPos;
buf.buffer.delete(startPos, endPos);
drawBuffer(endPos - startPos);
return true;
}
/**
* Implement the "vi" yank-to operation. This operation allows you
* to yank the contents of the current line based upon a move operation,
* for exaple "yw" yanks the current word, "3yw" yanks 3 words, etc.
*
* @param startPos The starting position from which to yank
* @param endPos The ending position to which to yank
* @return true if the yank succeeded
* @throws IOException
*/
private boolean viYankTo(int startPos, int endPos) throws IOException {
int cursorPos = startPos;
if (endPos < startPos) {
int tmp = endPos;
endPos = startPos;
startPos = tmp;
}
if (startPos == endPos) {
yankBuffer = "";
return true;
}
yankBuffer = buf.buffer.substring(startPos, endPos);
/*
* It was a movement command that moved the cursor to find the
* end position, so put the cursor back where it started.
*/
setCursorPosition(cursorPos);
return true;
}
/**
* Pasts the yank buffer to the right of the current cursor position
* and moves the cursor to the end of the pasted region.
*
* @param count Number of times to perform the operation.
* @return true if it worked, false otherwise
* @throws IOException
*/
private boolean viPut(int count) throws IOException {
if (yankBuffer.length () == 0) {
return true;
}
if (buf.cursor < buf.buffer.length ()) {
moveCursor(1);
}
for (int i = 0; i < count; i++) {
putString(yankBuffer);
}
moveCursor(-1);
return true;
}
/**
* Searches forward of the current position for a character and moves
* the cursor onto it.
* @param count Number of times to repeat the process.
* @param ch The character to search for
* @return true if the char was found, false otherwise
* @throws IOException
*/
private boolean viCharSearch(int count, int invokeChar, int ch) throws IOException {
if (ch < 0 || invokeChar < 0) {
return false;
}
char searchChar = (char)ch;
boolean isForward;
boolean stopBefore;
/*
* The character stuff turns out to be hairy. Here is how it works:
* f - search forward for ch
* F - search backward for ch
* t - search forward for ch, but stop just before the match
* T - search backward for ch, but stop just after the match
* ; - After [fFtT;], repeat the last search, after ',' reverse it
* , - After [fFtT;], reverse the last search, after ',' repeat it
*/
if (invokeChar == ';' || invokeChar == ',') {
// No recent search done? Then bail
if (charSearchChar == 0) {
return false;
}
// Reverse direction if switching between ',' and ';'
if (charSearchLastInvokeChar == ';' || charSearchLastInvokeChar == ',') {
if (charSearchLastInvokeChar != invokeChar) {
charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar);
}
}
else {
if (invokeChar == ',') {
charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar);
}
}
searchChar = charSearchChar;
}
else {
charSearchChar = searchChar;
charSearchFirstInvokeChar = (char) invokeChar;
}
charSearchLastInvokeChar = (char)invokeChar;
isForward = Character.isLowerCase(charSearchFirstInvokeChar);
stopBefore = (Character.toLowerCase(charSearchFirstInvokeChar) == 't');
boolean ok = false;
if (isForward) {
while (count-- > 0) {
int pos = buf.cursor + 1;
while (pos < buf.buffer.length()) {
if (buf.buffer.charAt(pos) == (char) searchChar) {
setCursorPosition(pos);
ok = true;
break;
}
++pos;
}
}
if (ok) {
if (stopBefore)
moveCursor(-1);
/*
* When in yank-to, move-to, del-to state we actually want to
* go to the character after the one we landed on to make sure
* that the character we ended up on is included in the
* operation
*/
if (isInViMoveOperationState()) {
moveCursor(1);
}
}
}
else {
while (count-- > 0) {
int pos = buf.cursor - 1;
while (pos >= 0) {
if (buf.buffer.charAt(pos) == (char) searchChar) {
setCursorPosition(pos);
ok = true;
break;
}
--pos;
}
}
if (ok && stopBefore)
moveCursor(1);
}
return ok;
}
private char switchCase(char ch) {
if (Character.isUpperCase(ch)) {
return Character.toLowerCase(ch);
}
return Character.toUpperCase(ch);
}
/**
* @return true if line reader is in the middle of doing a change-to
* delete-to or yank-to.
*/
private final boolean isInViMoveOperationState() {
return state == State.VI_CHANGE_TO
|| state == State.VI_DELETE_TO
|| state == State.VI_YANK_TO;
}
/**
* This is a close facsimile of the actual vi next word logic.
* As with viPreviousWord() this probably needs to be improved
* at some point.
*
* @param count number of iterations
* @return true if the move was successful, false otherwise
* @throws IOException
*/
private boolean viNextWord(int count) throws IOException {
int pos = buf.cursor;
int end = buf.buffer.length();
for (int i = 0; pos < end && i < count; i++) {
// Skip over letter/digits
while (pos < end && !isDelimiter(buf.buffer.charAt(pos))) {
++pos;
}
/*
* Don't you love special cases? During delete-to and yank-to
* operations the word movement is normal. However, during a
* change-to, the trailing spaces behind the last word are
* left in tact.
*/
if (i < (count-1) || !(state == State.VI_CHANGE_TO)) {
while (pos < end && isDelimiter(buf.buffer.charAt(pos))) {
++pos;
}
}
}
setCursorPosition(pos);
return true;
}
/**
* Implements a close facsimile of the vi end-of-word movement.
* If the character is on white space, it takes you to the end
* of the next word. If it is on the last character of a word
* it takes you to the next of the next word. Any other character
* of a word, takes you to the end of the current word.
*
* @param count Number of times to repeat the action
* @return true if it worked.
* @throws IOException
*/
private boolean viEndWord(int count) throws IOException {
int pos = buf.cursor;
int end = buf.buffer.length();
for (int i = 0; pos < end && i < count; i++) {
if (pos < (end-1)
&& !isDelimiter(buf.buffer.charAt(pos))
&& isDelimiter(buf.buffer.charAt (pos+1))) {
++pos;
}
// If we are on white space, then move back.
while (pos < end && isDelimiter(buf.buffer.charAt(pos))) {
++pos;
}
while (pos < (end-1) && !isDelimiter(buf.buffer.charAt(pos+1))) {
++pos;
}
}
setCursorPosition(pos);
return true;
}
private boolean previousWord() throws IOException {
while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) {
// nothing
}
while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) {
// nothing
}
return true;
}
private boolean nextWord() throws IOException {
while (isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) {
// nothing
}
while (!isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) {
// nothing
}
return true;
}
/**
* Deletes to the beginning of the word that the cursor is sitting on.
* If the cursor is on white-space, it deletes that and to the beginning
* of the word before it. If the user is not on a word or whitespace
* it deletes up to the end of the previous word.
*
* @param count Number of times to perform the operation
* @return true if it worked, false if you tried to delete too many words
* @throws IOException
*/
private boolean unixWordRubout(int count) throws IOException {
for (; count > 0; --count) {
if (buf.cursor == 0)
return false;
while (isWhitespace(buf.current()) && backspace()) {
// nothing
}
while (!isWhitespace(buf.current()) && backspace()) {
// nothing
}
}
return true;
}
private String insertComment(boolean isViMode) throws IOException {
String comment = this.getCommentBegin ();
setCursorPosition(0);
putString(comment);
if (isViMode) {
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
}
return accept();
}
/**
* Similar to putString() but allows the string to be repeated a specific
* number of times, allowing easy support of vi digit arguments to a given
* command. The string is placed as the current cursor position.
*
* @param count The count of times to insert the string.
* @param str The string to insert
* @return true if the operation is a success, false otherwise
* @throws IOException
*/
private boolean insert(int count, final CharSequence str) throws IOException {
for (int i = 0; i < count; i++) {
buf.write(str);
if (mask == null) {
// no masking
print(str);
} else if (mask == NULL_MASK) {
// don't print anything
} else {
print(mask, str.length());
}
}
drawBuffer();
return true;
}
/**
* Implements vi search ("/" or "?").
* @throws IOException
*/
private int viSearch(char searchChar) throws IOException {
boolean isForward = (searchChar == '/');
/*
* This is a little gross, I'm sure there is a more appropriate way
* of saving and restoring state.
*/
CursorBuffer origBuffer = buf.copy();
// Clear the contents of the current line and
setCursorPosition (0);
killLine();
// Our new "prompt" is the character that got us into search mode.
putString(Character.toString(searchChar));
flush();
boolean isAborted = false;
boolean isComplete = false;
/*
* Readline doesn't seem to do any special character map handling
* here, so I think we are safe.
*/
int ch = -1;
while (!isAborted && !isComplete && (ch = readCharacter()) != -1) {
switch (ch) {
case '\033': // ESC
/*
* The ESC behavior doesn't appear to be readline behavior,
* but it is a little tweak of my own. I like it.
*/
isAborted = true;
break;
case '\010': // Backspace
case '\177': // Delete
backspace();
/*
* Backspacing through the "prompt" aborts the search.
*/
if (buf.cursor == 0) {
isAborted = true;
}
break;
case '\012': // NL
case '\015': // CR
isComplete = true;
break;
default:
putString(Character.toString((char) ch));
}
flush();
}
// If we aborted, then put ourself at the end of the original buffer.
if (ch == -1 || isAborted) {
setCursorPosition(0);
killLine();
putString(origBuffer.buffer);
setCursorPosition(origBuffer.cursor);
return -1;
}
/*
* The first character of the buffer was the search character itself
* so we discard it.
*/
String searchTerm = buf.buffer.substring(1);
int idx = -1;
/*
* The semantics of the history thing is gross when you want to
* explicitly iterate over entries (without an iterator) as size()
* returns the actual number of entries in the list but get()
* doesn't work the way you think.
*/
int end = history.index();
int start = (end <= history.size()) ? 0 : end - history.size();
if (isForward) {
for (int i = start; i < end; i++) {
if (history.get(i).toString().contains(searchTerm)) {
idx = i;
break;
}
}
}
else {
for (int i = end-1; i >= start; i--) {
if (history.get(i).toString().contains(searchTerm)) {
idx = i;
break;
}
}
}
/*
* No match? Then restore what we were working on, but make sure
* the cursor is at the beginning of the line.
*/
if (idx == -1) {
setCursorPosition(0);
killLine();
putString(origBuffer.buffer);
setCursorPosition(0);
return -1;
}
/*
* Show the match.
*/
setCursorPosition(0);
killLine();
putString(history.get(idx));
setCursorPosition(0);
flush();
/*
* While searching really only the "n" and "N" keys are interpreted
* as movement, any other key is treated as if you are editing the
* line with it, so we return it back up to the caller for interpretation.
*/
isComplete = false;
while (!isComplete && (ch = readCharacter()) != -1) {
boolean forward = isForward;
switch (ch) {
case 'p': case 'P':
forward = !isForward;
// Fallthru
case 'n': case 'N':
boolean isMatch = false;
if (forward) {
for (int i = idx+1; !isMatch && i < end; i++) {
if (history.get(i).toString().contains(searchTerm)) {
idx = i;
isMatch = true;
}
}
}
else {
for (int i = idx - 1; !isMatch && i >= start; i--) {
if (history.get(i).toString().contains(searchTerm)) {
idx = i;
isMatch = true;
}
}
}
if (isMatch) {
setCursorPosition(0);
killLine();
putString(history.get(idx));
setCursorPosition(0);
}
break;
default:
isComplete = true;
}
flush();
}
/*
* Complete?
*/
return ch;
}
public void setParenBlinkTimeout(int timeout) {
parenBlinkTimeout = timeout;
}
private void insertClose(String s) throws IOException {
putString(s);
int closePosition = buf.cursor;
moveCursor(-1);
viMatch();
if (in.isNonBlockingEnabled()) {
in.peek(parenBlinkTimeout);
}
setCursorPosition(closePosition);
}
/**
* Implements vi style bracket matching ("%" command). The matching
* bracket for the current bracket type that you are sitting on is matched.
* The logic works like so:
* @return true if it worked, false if the cursor was not on a bracket
* character or if there was no matching bracket.
* @throws IOException
*/
private boolean viMatch() throws IOException {
int pos = buf.cursor;
if (pos == buf.length()) {
return false;
}
int type = getBracketType(buf.buffer.charAt (pos));
int move = (type < 0) ? -1 : 1;
int count = 1;
if (type == 0)
return false;
while (count > 0) {
pos += move;
// Fell off the start or end.
if (pos < 0 || pos >= buf.buffer.length ()) {
return false;
}
int curType = getBracketType(buf.buffer.charAt (pos));
if (curType == type) {
++count;
}
else if (curType == -type) {
--count;
}
}
/*
* Slight adjustment for delete-to, yank-to, change-to to ensure
* that the matching paren is consumed
*/
if (move > 0 && isInViMoveOperationState())
++pos;
setCursorPosition(pos);
return true;
}
/**
* Given a character determines what type of bracket it is (paren,
* square, curly, or none).
* @param ch The character to check
* @return 1 is square, 2 curly, 3 parent, or zero for none. The value
* will be negated if it is the closing form of the bracket.
*/
private int getBracketType (char ch) {
switch (ch) {
case '[': return 1;
case ']': return -1;
case '{': return 2;
case '}': return -2;
case '(': return 3;
case ')': return -3;
default:
return 0;
}
}
private boolean deletePreviousWord() throws IOException {
while (isDelimiter(buf.current()) && backspace()) {
// nothing
}
while (!isDelimiter(buf.current()) && backspace()) {
// nothing
}
return true;
}
private boolean deleteNextWord() throws IOException {
while (isDelimiter(buf.nextChar()) && delete()) {
}
while (!isDelimiter(buf.nextChar()) && delete()) {
// nothing
}
return true;
}
private boolean capitalizeWord() throws IOException {
boolean first = true;
int i = 1;
char c;
while (buf.cursor + i - 1< buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) {
buf.buffer.setCharAt(buf.cursor + i - 1, first ? Character.toUpperCase(c) : Character.toLowerCase(c));
first = false;
i++;
}
drawBuffer();
moveCursor(i - 1);
return true;
}
private boolean upCaseWord() throws IOException {
int i = 1;
char c;
while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) {
buf.buffer.setCharAt(buf.cursor + i - 1, Character.toUpperCase(c));
i++;
}
drawBuffer();
moveCursor(i - 1);
return true;
}
private boolean downCaseWord() throws IOException {
int i = 1;
char c;
while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) {
buf.buffer.setCharAt(buf.cursor + i - 1, Character.toLowerCase(c));
i++;
}
drawBuffer();
moveCursor(i - 1);
return true;
}
/**
* Performs character transpose. The character prior to the cursor and the
* character under the cursor are swapped and the cursor is advanced one
* character unless you are already at the end of the line.
*
* @param count The number of times to perform the transpose
* @return true if the operation succeeded, false otherwise (e.g. transpose
* cannot happen at the beginning of the line).
* @throws IOException
*/
private boolean transposeChars(int count) throws IOException {
for (; count > 0; --count) {
if (buf.cursor == 0 || buf.cursor == buf.buffer.length()) {
return false;
}
int first = buf.cursor-1;
int second = buf.cursor;
char tmp = buf.buffer.charAt (first);
buf.buffer.setCharAt(first, buf.buffer.charAt(second));
buf.buffer.setCharAt(second, tmp);
// This could be done more efficiently by only re-drawing at the end.
moveInternal(-1);
drawBuffer();
moveInternal(2);
}
return true;
}
public boolean isKeyMap(String name) {
// Current keymap.
KeyMap map = consoleKeys.getKeys();
KeyMap mapByName = consoleKeys.getKeyMaps().get(name);
if (mapByName == null)
return false;
/*
* This may not be safe to do, but there doesn't appear to be a
* clean way to find this information out.
*/
return map == mapByName;
}
/**
* The equivalent of hitting <RET>. The line is considered
* complete and is returned.
*
* @return The completed line of text.
* @throws IOException
*/
public String accept() throws IOException {
moveToEnd();
println(); // output newline
flush();
return finishBuffer();
}
private void abort() throws IOException {
beep();
buf.clear();
println();
redrawLine();
}
/**
* Move the cursor where characters.
*
* @param num If less than 0, move abs(where) to the left, otherwise move where to the right.
* @return The number of spaces we moved
*/
public int moveCursor(final int num) throws IOException {
int where = num;
if ((buf.cursor == 0) && (where <= 0)) {
return 0;
}
if ((buf.cursor == buf.buffer.length()) && (where >= 0)) {
return 0;
}
if ((buf.cursor + where) < 0) {
where = -buf.cursor;
}
else if ((buf.cursor + where) > buf.buffer.length()) {
where = buf.buffer.length() - buf.cursor;
}
moveInternal(where);
return where;
}
/**
* Move the cursor where characters, without checking the current buffer.
*
* @param where the number of characters to move to the right or left.
*/
private void moveInternal(final int where) throws IOException {
// debug ("move cursor " + where + " ("
// + buf.cursor + " => " + (buf.cursor + where) + ")");
buf.cursor += where;
if (terminal.isAnsiSupported()) {
if (where < 0) {
back(Math.abs(where));
} else {
int width = getTerminal().getWidth();
int cursor = getCursorPosition();
int oldLine = (cursor - where) / width;
int newLine = cursor / width;
if (newLine > oldLine) {
printAnsiSequence((newLine - oldLine) + "B");
}
printAnsiSequence(1 +(cursor % width) + "G");
}
// flush();
return;
}
char c;
if (where < 0) {
int len = 0;
for (int i = buf.cursor; i < buf.cursor - where; i++) {
if (buf.buffer.charAt(i) == '\t') {
len += TAB_WIDTH;
}
else {
len++;
}
}
char chars[] = new char[len];
Arrays.fill(chars, BACKSPACE);
out.write(chars);
return;
}
else if (buf.cursor == 0) {
return;
}
else if (mask != null) {
c = mask;
}
else {
print(buf.buffer.substring(buf.cursor - where, buf.cursor).toCharArray());
return;
}
// null character mask: don't output anything
if (mask == NULL_MASK) {
return;
}
print(c, Math.abs(where));
}
// FIXME: replace() is not used
public final boolean replace(final int num, final String replacement) {
buf.buffer.replace(buf.cursor - num, buf.cursor, replacement);
try {
moveCursor(-num);
drawBuffer(Math.max(0, num - replacement.length()));
moveCursor(replacement.length());
}
catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* Read a character from the console.
*
* @return the character, or -1 if an EOF is received.
*/
public final int readCharacter() throws IOException {
int c = reader.read();
if (c >= 0) {
Log.trace("Keystroke: ", c);
// clear any echo characters
if (terminal.isSupported()) {
clearEcho(c);
}
}
return c;
}
/**
* Clear the echoed characters for the specified character code.
*/
private int clearEcho(final int c) throws IOException {
// if the terminal is not echoing, then ignore
if (!terminal.isEchoEnabled()) {
return 0;
}
// otherwise, clear
int num = countEchoCharacters(c);
back(num);
drawBuffer(num);
return num;
}
private int countEchoCharacters(final int c) {
// tabs as special: we need to determine the number of spaces
// to cancel based on what out current cursor position is
if (c == 9) {
int tabStop = 8; // will this ever be different?
int position = getCursorPosition();
return tabStop - (position % tabStop);
}
return getPrintableCharacters(c).length();
}
/**
* Return the number of characters that will be printed when the specified
* character is echoed to the screen
*
* Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie.
*/
private StringBuilder getPrintableCharacters(final int ch) {
StringBuilder sbuff = new StringBuilder();
if (ch >= 32) {
if (ch < 127) {
sbuff.append(ch);
}
else if (ch == 127) {
sbuff.append('^');
sbuff.append('?');
}
else {
sbuff.append('M');
sbuff.append('-');
if (ch >= (128 + 32)) {
if (ch < (128 + 127)) {
sbuff.append((char) (ch - 128));
}
else {
sbuff.append('^');
sbuff.append('?');
}
}
else {
sbuff.append('^');
sbuff.append((char) (ch - 128 + 64));
}
}
}
else {
sbuff.append('^');
sbuff.append((char) (ch + 64));
}
return sbuff;
}
public final int readCharacter(final char... allowed) throws IOException {
// if we restrict to a limited set and the current character is not in the set, then try again.
char c;
Arrays.sort(allowed); // always need to sort before binarySearch
while (Arrays.binarySearch(allowed, c = (char) readCharacter()) < 0) {
// nothing
}
return c;
}
//
// Key Bindings
//
public static final String JLINE_COMPLETION_THRESHOLD = "jline.completion.threshold";
//
// Line Reading
//
/**
* Read the next line and return the contents of the buffer.
*/
public String readLine() throws IOException {
return readLine((String) null);
}
/**
* Read the next line with the specified character mask. If null, then
* characters will be echoed. If 0, then no characters will be echoed.
*/
public String readLine(final Character mask) throws IOException {
return readLine(null, mask);
}
public String readLine(final String prompt) throws IOException {
return readLine(prompt, null);
}
/**
* Sets the current keymap by name. Supported keymaps are "emacs",
* "vi-insert", "vi-move".
* @param name The name of the keymap to switch to
* @return true if the keymap was set, or false if the keymap is
* not recognized.
*/
public boolean setKeyMap(String name) {
return consoleKeys.setKeyMap(name);
}
/**
* Returns the name of the current key mapping.
* @return the name of the key mapping. This will be the canonical name
* of the current mode of the key map and may not reflect the name that
* was used with {@link #setKeyMap(String)}.
*/
public String getKeyMap() {
return consoleKeys.getKeys().getName();
}
/**
* Read a line from the in {@link InputStream}, and return the line
* (without any trailing newlines).
*
* @param prompt The prompt to issue to the console, may be null.
* @return A line that is read from the terminal, or null if there was null input (e.g., CTRL-D
* was pressed).
*/
public String readLine(String prompt, final Character mask) throws IOException {
// prompt may be null
// mask may be null
/*
* This is the accumulator for VI-mode repeat count. That is, while in
* move mode, if you type 30x it will delete 30 characters. This is
* where the "30" is accumulated until the command is struck.
*/
int repeatCount = 0;
// FIXME: This blows, each call to readLine will reset the console's state which doesn't seem very nice.
this.mask = mask;
if (prompt != null) {
setPrompt(prompt);
}
else {
prompt = getPrompt();
}
try {
if (!terminal.isSupported()) {
beforeReadLine(prompt, mask);
}
if (prompt != null && prompt.length() > 0) {
out.write(prompt);
out.flush();
}
// if the terminal is unsupported, just use plain-java reading
if (!terminal.isSupported()) {
return readLineSimple();
}
if (handleUserInterrupt && (terminal instanceof UnixTerminal)) {
((UnixTerminal) terminal).disableInterruptCharacter();
}
String originalPrompt = this.prompt;
state = State.NORMAL;
boolean success = true;
StringBuilder sb = new StringBuilder();
Stack pushBackChar = new Stack();
while (true) {
int c = pushBackChar.isEmpty() ? readCharacter() : pushBackChar.pop ();
if (c == -1) {
return null;
}
sb.appendCodePoint(c);
if (recording) {
macro += new String(new int[]{c}, 0, 1);
}
Object o = getKeys().getBound( sb );
if (o == Operation.DO_LOWERCASE_VERSION) {
sb.setLength( sb.length() - 1);
sb.append( Character.toLowerCase( (char) c ));
o = getKeys().getBound( sb );
}
/*
* A KeyMap indicates that the key that was struck has a
* number of keys that can follow it as indicated in the
* map. This is used primarily for Emacs style ESC-META-x
* lookups. Since more keys must follow, go back to waiting
* for the next key.
*/
if ( o instanceof KeyMap ) {
/*
* The ESC key (#27) is special in that it is ambiguous until
* you know what is coming next. The ESC could be a literal
* escape, like the user entering vi-move mode, or it could
* be part of a terminal control sequence. The following
* logic attempts to disambiguate things in the same
* fashion as regular vi or readline.
*
* When ESC is encountered and there is no other pending
* character in the pushback queue, then attempt to peek
* into the input stream (if the feature is enabled) for
* 150ms. If nothing else is coming, then assume it is
* not a terminal control sequence, but a raw escape.
*/
if (c == 27
&& pushBackChar.isEmpty()
&& in.isNonBlockingEnabled()
&& in.peek(escapeTimeout) == -2) {
o = ((KeyMap) o).getAnotherKey();
if (o == null || o instanceof KeyMap) {
continue;
}
sb.setLength(0);
}
else {
continue;
}
}
/*
* If we didn't find a binding for the key and there is
* more than one character accumulated then start checking
* the largest span of characters from the beginning to
* see if there is a binding for them.
*
* For example if our buffer has ESC,CTRL-M,C the getBound()
* called previously indicated that there is no binding for
* this sequence, so this then checks ESC,CTRL-M, and failing
* that, just ESC. Each keystroke that is pealed off the end
* during these tests is stuffed onto the pushback buffer so
* they won't be lost.
*
* If there is no binding found, then we go back to waiting for
* input.
*/
while ( o == null && sb.length() > 0 ) {
c = sb.charAt( sb.length() - 1 );
sb.setLength( sb.length() - 1 );
Object o2 = getKeys().getBound( sb );
if ( o2 instanceof KeyMap ) {
o = ((KeyMap) o2).getAnotherKey();
if ( o == null ) {
continue;
} else {
pushBackChar.push( (char) c );
}
}
}
if ( o == null ) {
continue;
}
Log.trace("Binding: ", o);
// Handle macros
if (o instanceof String) {
String macro = (String) o;
for (int i = 0; i < macro.length(); i++) {
pushBackChar.push(macro.charAt(macro.length() - 1 - i));
}
sb.setLength( 0 );
continue;
}
// Handle custom callbacks
if (o instanceof ActionListener) {
((ActionListener) o).actionPerformed(null);
sb.setLength( 0 );
continue;
}
// Search mode.
//
// Note that we have to do this first, because if there is a command
// not linked to a search command, we leave the search mode and fall
// through to the normal state.
if (state == State.SEARCH || state == State.FORWARD_SEARCH) {
int cursorDest = -1;
switch ( ((Operation) o )) {
case ABORT:
state = State.NORMAL;
buf.clear();
buf.buffer.append(searchTerm);
break;
case REVERSE_SEARCH_HISTORY:
case HISTORY_SEARCH_BACKWARD:
state = State.SEARCH;
if (searchTerm.length() == 0) {
searchTerm.append(previousSearchTerm);
}
if (searchIndex > 0) {
searchIndex = searchBackwards(searchTerm.toString(), searchIndex);
}
break;
case FORWARD_SEARCH_HISTORY:
case HISTORY_SEARCH_FORWARD:
state = State.FORWARD_SEARCH;
if (searchTerm.length() == 0) {
searchTerm.append(previousSearchTerm);
}
if (searchIndex > -1 && searchIndex < history.size() - 1) {
searchIndex = searchForwards(searchTerm.toString(), searchIndex);
}
break;
case BACKWARD_DELETE_CHAR:
if (searchTerm.length() > 0) {
searchTerm.deleteCharAt(searchTerm.length() - 1);
if (state == State.SEARCH) {
searchIndex = searchBackwards(searchTerm.toString());
} else {
searchIndex = searchForwards(searchTerm.toString());
}
}
break;
case SELF_INSERT:
searchTerm.appendCodePoint(c);
if (state == State.SEARCH) {
searchIndex = searchBackwards(searchTerm.toString());
} else {
searchIndex = searchForwards(searchTerm.toString());
}
break;
default:
// Set buffer and cursor position to the found string.
if (searchIndex != -1) {
history.moveTo(searchIndex);
// set cursor position to the found string
cursorDest = history.current().toString().indexOf(searchTerm.toString());
}
state = State.NORMAL;
break;
}
// if we're still in search mode, print the search status
if (state == State.SEARCH || state == State.FORWARD_SEARCH) {
if (searchTerm.length() == 0) {
if (state == State.SEARCH) {
printSearchStatus("", "");
} else {
printForwardSearchStatus("", "");
}
searchIndex = -1;
} else {
if (searchIndex == -1) {
beep();
printSearchStatus(searchTerm.toString(), "");
} else if (state == State.SEARCH) {
printSearchStatus(searchTerm.toString(), history.get(searchIndex).toString());
} else {
printForwardSearchStatus(searchTerm.toString(), history.get(searchIndex).toString());
}
}
}
// otherwise, restore the line
else {
restoreLine(originalPrompt, cursorDest);
}
}
if (state != State.SEARCH && state != State.FORWARD_SEARCH) {
/*
* If this is still false at the end of the switch, then
* we reset our repeatCount to 0.
*/
boolean isArgDigit = false;
/*
* Every command that can be repeated a specified number
* of times, needs to know how many times to repeat, so
* we figure that out here.
*/
int count = (repeatCount == 0) ? 1 : repeatCount;
/*
* Default success to true. You only need to explicitly
* set it if something goes wrong.
*/
success = true;
if (o instanceof Operation) {
Operation op = (Operation)o;
/*
* Current location of the cursor (prior to the operation).
* These are used by vi *-to operation (e.g. delete-to)
* so we know where we came from.
*/
int cursorStart = buf.cursor;
State origState = state;
/*
* If we are on a "vi" movement based operation, then we
* need to restrict the sets of inputs pretty heavily.
*/
if (state == State.VI_CHANGE_TO
|| state == State.VI_YANK_TO
|| state == State.VI_DELETE_TO) {
op = viDeleteChangeYankToRemap(op);
}
switch ( op ) {
case COMPLETE: // tab
// There is an annoyance with tab completion in that
// sometimes the user is actually pasting input in that
// has physical tabs in it. This attempts to look at how
// quickly a character follows the tab, if the character
// follows *immediately*, we assume it is a tab literal.
boolean isTabLiteral = false;
if (copyPasteDetection
&& c == 9
&& (!pushBackChar.isEmpty()
|| (in.isNonBlockingEnabled() && in.peek(escapeTimeout) != -2))) {
isTabLiteral = true;
}
if (! isTabLiteral) {
success = complete();
}
else {
putString(sb);
}
break;
case POSSIBLE_COMPLETIONS:
printCompletionCandidates();
break;
case BEGINNING_OF_LINE:
success = setCursorPosition(0);
break;
case KILL_LINE: // CTRL-K
success = killLine();
break;
case KILL_WHOLE_LINE:
success = setCursorPosition(0) && killLine();
break;
case CLEAR_SCREEN: // CTRL-L
success = clearScreen();
break;
case OVERWRITE_MODE:
buf.setOverTyping(!buf.isOverTyping());
break;
case SELF_INSERT:
putString(sb);
break;
case ACCEPT_LINE:
return accept();
case ABORT:
if (searchTerm == null) {
abort();
}
break;
case INTERRUPT:
if (handleUserInterrupt) {
println();
flush();
String partialLine = buf.buffer.toString();
buf.clear();
throw new UserInterruptException(partialLine);
}
break;
/*
* VI_MOVE_ACCEPT_LINE is the result of an ENTER
* while in move mode. This is the same as a normal
* ACCEPT_LINE, except that we need to enter
* insert mode as well.
*/
case VI_MOVE_ACCEPT_LINE:
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
return accept();
case BACKWARD_WORD:
success = previousWord();
break;
case FORWARD_WORD:
success = nextWord();
break;
case PREVIOUS_HISTORY:
success = moveHistory(false);
break;
/*
* According to bash/readline move through history
* in "vi" mode will move the cursor to the
* start of the line. If there is no previous
* history, then the cursor doesn't move.
*/
case VI_PREVIOUS_HISTORY:
success = moveHistory(false, count)
&& setCursorPosition(0);
break;
case NEXT_HISTORY:
success = moveHistory(true);
break;
/*
* According to bash/readline move through history
* in "vi" mode will move the cursor to the
* start of the line. If there is no next history,
* then the cursor doesn't move.
*/
case VI_NEXT_HISTORY:
success = moveHistory(true, count)
&& setCursorPosition(0);
break;
case BACKWARD_DELETE_CHAR: // backspace
success = backspace();
break;
case EXIT_OR_DELETE_CHAR:
if (buf.buffer.length() == 0) {
return null;
}
success = deleteCurrentCharacter();
break;
case DELETE_CHAR: // delete
success = deleteCurrentCharacter();
break;
case BACKWARD_CHAR:
success = moveCursor(-(count)) != 0;
break;
case FORWARD_CHAR:
success = moveCursor(count) != 0;
break;
case UNIX_LINE_DISCARD:
success = resetLine();
break;
case UNIX_WORD_RUBOUT:
success = unixWordRubout(count);
break;
case BACKWARD_KILL_WORD:
success = deletePreviousWord();
break;
case KILL_WORD:
success = deleteNextWord();
break;
case BEGINNING_OF_HISTORY:
success = history.moveToFirst();
if (success) {
setBuffer(history.current());
}
break;
case END_OF_HISTORY:
success = history.moveToLast();
if (success) {
setBuffer(history.current());
}
break;
case REVERSE_SEARCH_HISTORY:
case HISTORY_SEARCH_BACKWARD:
if (searchTerm != null) {
previousSearchTerm = searchTerm.toString();
}
searchTerm = new StringBuffer(buf.buffer);
state = State.SEARCH;
if (searchTerm.length() > 0) {
searchIndex = searchBackwards(searchTerm.toString());
if (searchIndex == -1) {
beep();
}
printSearchStatus(searchTerm.toString(),
searchIndex > -1 ? history.get(searchIndex).toString() : "");
} else {
searchIndex = -1;
printSearchStatus("", "");
}
break;
case FORWARD_SEARCH_HISTORY:
case HISTORY_SEARCH_FORWARD:
if (searchTerm != null) {
previousSearchTerm = searchTerm.toString();
}
searchTerm = new StringBuffer(buf.buffer);
state = State.FORWARD_SEARCH;
if (searchTerm.length() > 0) {
searchIndex = searchForwards(searchTerm.toString());
if (searchIndex == -1) {
beep();
}
printForwardSearchStatus(searchTerm.toString(),
searchIndex > -1 ? history.get(searchIndex).toString() : "");
} else {
searchIndex = -1;
printForwardSearchStatus("", "");
}
break;
case CAPITALIZE_WORD:
success = capitalizeWord();
break;
case UPCASE_WORD:
success = upCaseWord();
break;
case DOWNCASE_WORD:
success = downCaseWord();
break;
case END_OF_LINE:
success = moveToEnd();
break;
case TAB_INSERT:
putString( "\t" );
break;
case RE_READ_INIT_FILE:
consoleKeys.loadKeys(appName, inputrcUrl);
break;
case START_KBD_MACRO:
recording = true;
break;
case END_KBD_MACRO:
recording = false;
macro = macro.substring(0, macro.length() - sb.length());
break;
case CALL_LAST_KBD_MACRO:
for (int i = 0; i < macro.length(); i++) {
pushBackChar.push(macro.charAt(macro.length() - 1 - i));
}
sb.setLength( 0 );
break;
case VI_EDITING_MODE:
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
break;
case VI_MOVEMENT_MODE:
/*
* If we are re-entering move mode from an
* aborted yank-to, delete-to, change-to then
* don't move the cursor back. The cursor is
* only move on an expclit entry to movement
* mode.
*/
if (state == state.NORMAL) {
moveCursor(-1);
}
consoleKeys.setKeyMap(KeyMap.VI_MOVE);
break;
case VI_INSERTION_MODE:
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
break;
case VI_APPEND_MODE:
moveCursor(1);
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
break;
case VI_APPEND_EOL:
success = moveToEnd();
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
break;
/*
* Handler for CTRL-D. Attempts to follow readline
* behavior. If the line is empty, then it is an EOF
* otherwise it is as if the user hit enter.
*/
case VI_EOF_MAYBE:
if (buf.buffer.length() == 0) {
return null;
}
return accept();
case TRANSPOSE_CHARS:
success = transposeChars(count);
break;
case INSERT_COMMENT:
return insertComment (false);
case INSERT_CLOSE_CURLY:
insertClose("}");
break;
case INSERT_CLOSE_PAREN:
insertClose(")");
break;
case INSERT_CLOSE_SQUARE:
insertClose("]");
break;
case VI_INSERT_COMMENT:
return insertComment (true);
case VI_MATCH:
success = viMatch ();
break;
case VI_SEARCH:
int lastChar = viSearch(sb.charAt (0));
if (lastChar != -1) {
pushBackChar.push((char)lastChar);
}
break;
case VI_ARG_DIGIT:
repeatCount = (repeatCount * 10) + sb.charAt(0) - '0';
isArgDigit = true;
break;
case VI_BEGNNING_OF_LINE_OR_ARG_DIGIT:
if (repeatCount > 0) {
repeatCount = (repeatCount * 10) + sb.charAt(0) - '0';
isArgDigit = true;
}
else {
success = setCursorPosition(0);
}
break;
case VI_PREV_WORD:
success = viPreviousWord(count);
break;
case VI_NEXT_WORD:
success = viNextWord(count);
break;
case VI_END_WORD:
success = viEndWord(count);
break;
case VI_INSERT_BEG:
success = setCursorPosition(0);
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
break;
case VI_RUBOUT:
success = viRubout(count);
break;
case VI_DELETE:
success = viDelete(count);
break;
case VI_DELETE_TO:
/*
* This is a weird special case. In vi
* "dd" deletes the current line. So if we
* get a delete-to, followed by a delete-to,
* we delete the line.
*/
if (state == State.VI_DELETE_TO) {
success = setCursorPosition(0) && killLine();
state = origState = State.NORMAL;
}
else {
state = State.VI_DELETE_TO;
}
break;
case VI_YANK_TO:
// Similar to delete-to, a "yy" yanks the whole line.
if (state == State.VI_YANK_TO) {
yankBuffer = buf.buffer.toString();
state = origState = State.NORMAL;
}
else {
state = State.VI_YANK_TO;
}
break;
case VI_CHANGE_TO:
if (state == State.VI_CHANGE_TO) {
success = setCursorPosition(0) && killLine();
state = origState = State.NORMAL;
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
}
else {
state = State.VI_CHANGE_TO;
}
break;
case VI_PUT:
success = viPut(count);
break;
case VI_CHAR_SEARCH: {
// ';' and ',' don't need another character. They indicate repeat next or repeat prev.
int searchChar = (c != ';' && c != ',')
? (pushBackChar.isEmpty()
? readCharacter()
: pushBackChar.pop ())
: 0;
success = viCharSearch(count, c, searchChar);
}
break;
case VI_CHANGE_CASE:
success = viChangeCase(count);
break;
case VI_CHANGE_CHAR:
success = viChangeChar(count,
pushBackChar.isEmpty()
? readCharacter()
: pushBackChar.pop());
break;
case EMACS_EDITING_MODE:
consoleKeys.setKeyMap(KeyMap.EMACS);
break;
default:
break;
}
/*
* If we were in a yank-to, delete-to, move-to
* when this operation started, then fall back to
*/
if (origState != State.NORMAL) {
if (origState == State.VI_DELETE_TO) {
success = viDeleteTo(cursorStart, buf.cursor);
}
else if (origState == State.VI_CHANGE_TO) {
success = viDeleteTo(cursorStart, buf.cursor);
consoleKeys.setKeyMap(KeyMap.VI_INSERT);
}
else if (origState == State.VI_YANK_TO) {
success = viYankTo(cursorStart, buf.cursor);
}
state = State.NORMAL;
}
/*
* Another subtly. The check for the NORMAL state is
* to ensure that we do not clear out the repeat
* count when in delete-to, yank-to, or move-to modes.
*/
if (state == State.NORMAL && !isArgDigit) {
/*
* If the operation performed wasn't a vi argument
* digit, then clear out the current repeatCount;
*/
repeatCount = 0;
}
if (state != State.SEARCH && state != State.FORWARD_SEARCH) {
previousSearchTerm = "";
searchTerm = null;
searchIndex = -1;
}
}
}
if (!success) {
beep();
}
sb.setLength( 0 );
flush();
}
}
finally {
if (!terminal.isSupported()) {
afterReadLine();
}
if (handleUserInterrupt && (terminal instanceof UnixTerminal)) {
((UnixTerminal) terminal).enableInterruptCharacter();
}
}
}
/**
* Read a line for unsupported terminals.
*/
private String readLineSimple() throws IOException {
StringBuilder buff = new StringBuilder();
if (skipLF) {
skipLF = false;
int i = readCharacter();
if (i == -1 || i == '\r') {
return buff.toString();
} else if (i == '\n') {
// ignore
} else {
buff.append((char) i);
}
}
while (true) {
int i = readCharacter();
if (i == -1 && buff.length() == 0) {
return null;
}
if (i == -1 || i == '\n') {
return buff.toString();
} else if (i == '\r') {
skipLF = true;
return buff.toString();
} else {
buff.append((char) i);
}
}
}
//
// Completion
//
private final List completers = new LinkedList();
private CompletionHandler completionHandler = new CandidateListCompletionHandler();
/**
* Add the specified {@link jline.console.completer.Completer} to the list of handlers for tab-completion.
*
* @param completer the {@link jline.console.completer.Completer} to add
* @return true if it was successfully added
*/
public boolean addCompleter(final Completer completer) {
return completers.add(completer);
}
/**
* Remove the specified {@link jline.console.completer.Completer} from the list of handlers for tab-completion.
*
* @param completer The {@link Completer} to remove
* @return True if it was successfully removed
*/
public boolean removeCompleter(final Completer completer) {
return completers.remove(completer);
}
/**
* Returns an unmodifiable list of all the completers.
*/
public Collection getCompleters() {
return Collections.unmodifiableList(completers);
}
public void setCompletionHandler(final CompletionHandler handler) {
this.completionHandler = checkNotNull(handler);
}
public CompletionHandler getCompletionHandler() {
return this.completionHandler;
}
/**
* Use the completers to modify the buffer with the appropriate completions.
*
* @return true if successful
*/
protected boolean complete() throws IOException {
// debug ("tab for (" + buf + ")");
if (completers.size() == 0) {
return false;
}
List candidates = new LinkedList();
String bufstr = buf.buffer.toString();
int cursor = buf.cursor;
int position = -1;
for (Completer comp : completers) {
if ((position = comp.complete(bufstr, cursor, candidates)) != -1) {
break;
}
}
return candidates.size() != 0 && getCompletionHandler().complete(this, candidates, position);
}
protected void printCompletionCandidates() throws IOException {
// debug ("tab for (" + buf + ")");
if (completers.size() == 0) {
return;
}
List candidates = new LinkedList();
String bufstr = buf.buffer.toString();
int cursor = buf.cursor;
for (Completer comp : completers) {
if (comp.complete(bufstr, cursor, candidates) != -1) {
break;
}
}
CandidateListCompletionHandler.printCandidates(this, candidates);
drawLine();
}
/**
* The number of tab-completion candidates above which a warning will be
* prompted before showing all the candidates.
*/
private int autoprintThreshold = Configuration.getInteger(JLINE_COMPLETION_THRESHOLD, 100); // same default as bash
/**
* @param threshold the number of candidates to print without issuing a warning.
*/
public void setAutoprintThreshold(final int threshold) {
this.autoprintThreshold = threshold;
}
/**
* @return the number of candidates to print without issuing a warning.
*/
public int getAutoprintThreshold() {
return autoprintThreshold;
}
private boolean paginationEnabled;
/**
* Whether to use pagination when the number of rows of candidates exceeds the height of the terminal.
*/
public void setPaginationEnabled(final boolean enabled) {
this.paginationEnabled = enabled;
}
/**
* Whether to use pagination when the number of rows of candidates exceeds the height of the terminal.
*/
public boolean isPaginationEnabled() {
return paginationEnabled;
}
//
// History
//
private History history = new MemoryHistory();
public void setHistory(final History history) {
this.history = history;
}
public History getHistory() {
return history;
}
private boolean historyEnabled = true;
/**
* Whether or not to add new commands to the history buffer.
*/
public void setHistoryEnabled(final boolean enabled) {
this.historyEnabled = enabled;
}
/**
* Whether or not to add new commands to the history buffer.
*/
public boolean isHistoryEnabled() {
return historyEnabled;
}
/**
* Used in "vi" mode for argumented history move, to move a specific
* number of history entries forward or back.
*
* @param next If true, move forward
* @param count The number of entries to move
* @return true if the move was successful
* @throws IOException
*/
private boolean moveHistory(final boolean next, int count) throws IOException {
boolean ok = true;
for (int i = 0; i < count && (ok = moveHistory(next)); i++) {
/* empty */
}
return ok;
}
/**
* Move up or down the history tree.
*/
private boolean moveHistory(final boolean next) throws IOException {
if (next && !history.next()) {
return false;
}
else if (!next && !history.previous()) {
return false;
}
setBuffer(history.current());
return true;
}
//
// Printing
//
public static final String CR = Configuration.getLineSeparator();
/**
* Output the specified character to the output stream without manipulating the current buffer.
*/
private void print(final int c) throws IOException {
if (c == '\t') {
char chars[] = new char[TAB_WIDTH];
Arrays.fill(chars, ' ');
out.write(chars);
return;
}
out.write(c);
}
/**
* Output the specified characters to the output stream without manipulating the current buffer.
*/
private void print(final char... buff) throws IOException {
int len = 0;
for (char c : buff) {
if (c == '\t') {
len += TAB_WIDTH;
}
else {
len++;
}
}
char chars[];
if (len == buff.length) {
chars = buff;
}
else {
chars = new char[len];
int pos = 0;
for (char c : buff) {
if (c == '\t') {
Arrays.fill(chars, pos, pos + TAB_WIDTH, ' ');
pos += TAB_WIDTH;
}
else {
chars[pos] = c;
pos++;
}
}
}
out.write(chars);
}
private void print(final char c, final int num) throws IOException {
if (num == 1) {
print(c);
}
else {
char[] chars = new char[num];
Arrays.fill(chars, c);
print(chars);
}
}
/**
* Output the specified string to the output stream (but not the buffer).
*/
public final void print(final CharSequence s) throws IOException {
print(checkNotNull(s).toString().toCharArray());
}
public final void println(final CharSequence s) throws IOException {
print(checkNotNull(s).toString().toCharArray());
println();
}
/**
* Output a platform-dependant newline.
*/
public final void println() throws IOException {
print(CR);
// flush();
}
//
// Actions
//
/**
* Issue a delete.
*
* @return true if successful
*/
public final boolean delete() throws IOException {
return delete(1) == 1;
}
// FIXME: delete(int) only used by above + the return is always 1 and num is ignored
/**
* Issue num deletes.
*
* @return the number of characters backed up
*/
private int delete(final int num) throws IOException {
// TODO: Try to use jansi for this
/* Commented out because of DWA-2949:
if (buf.cursor == 0) {
return 0;
}
*/
buf.buffer.delete(buf.cursor, buf.cursor + 1);
drawBuffer(1);
return 1;
}
/**
* Kill the buffer ahead of the current cursor position.
*
* @return true if successful
*/
public boolean killLine() throws IOException {
int cp = buf.cursor;
int len = buf.buffer.length();
if (cp >= len) {
return false;
}
int num = buf.buffer.length() - cp;
clearAhead(num, 0);
for (int i = 0; i < num; i++) {
buf.buffer.deleteCharAt(len - i - 1);
}
return true;
}
/**
* Clear the screen by issuing the ANSI "clear screen" code.
*/
public boolean clearScreen() throws IOException {
if (!terminal.isAnsiSupported()) {
return false;
}
// send the ANSI code to clear the screen
printAnsiSequence("2J");
// then send the ANSI code to go to position 1,1
printAnsiSequence("1;1H");
redrawLine();
return true;
}
/**
* Issue an audible keyboard bell.
*/
public void beep() throws IOException {
if (bellEnabled) {
print(KEYBOARD_BELL);
// need to flush so the console actually beeps
flush();
}
}
/**
* Paste the contents of the clipboard into the console buffer
*
* @return true if clipboard contents pasted
*/
public boolean paste() throws IOException {
Clipboard clipboard;
try { // May throw ugly exception on system without X
clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
}
catch (Exception e) {
return false;
}
if (clipboard == null) {
return false;
}
Transferable transferable = clipboard.getContents(null);
if (transferable == null) {
return false;
}
try {
Object content = transferable.getTransferData(DataFlavor.plainTextFlavor);
// This fix was suggested in bug #1060649 at
// http://sourceforge.net/tracker/index.php?func=detail&aid=1060649&group_id=64033&atid=506056
// to get around the deprecated DataFlavor.plainTextFlavor, but it
// raises a UnsupportedFlavorException on Mac OS X
if (content == null) {
try {
content = new DataFlavor().getReaderForText(transferable);
}
catch (Exception e) {
// ignore
}
}
if (content == null) {
return false;
}
String value;
if (content instanceof Reader) {
// TODO: we might want instead connect to the input stream
// so we can interpret individual lines
value = "";
String line;
BufferedReader read = new BufferedReader((Reader) content);
while ((line = read.readLine()) != null) {
if (value.length() > 0) {
value += "\n";
}
value += line;
}
}
else {
value = content.toString();
}
if (value == null) {
return true;
}
putString(value);
return true;
}
catch (UnsupportedFlavorException e) {
Log.error("Paste failed: ", e);
return false;
}
}
//
// Triggered Actions
//
private final Map triggeredActions = new HashMap();
/**
* Adding a triggered Action allows to give another curse of action if a character passed the pre-processing.
*
* Say you want to close the application if the user enter q.
* addTriggerAction('q', new ActionListener(){ System.exit(0); }); would do the trick.
*/
public void addTriggeredAction(final char c, final ActionListener listener) {
triggeredActions.put(c, listener);
}
//
// Formatted Output
//
/**
* Output the specified {@link Collection} in proper columns.
*/
public void printColumns(final Collection extends CharSequence> items) throws IOException {
if (items == null || items.isEmpty()) {
return;
}
int width = getTerminal().getWidth();
int height = getTerminal().getHeight();
int maxWidth = 0;
for (CharSequence item : items) {
maxWidth = Math.max(maxWidth, item.length());
}
maxWidth = maxWidth + 3;
Log.debug("Max width: ", maxWidth);
int showLines;
if (isPaginationEnabled()) {
showLines = height - 1; // page limit
}
else {
showLines = Integer.MAX_VALUE;
}
StringBuilder buff = new StringBuilder();
for (CharSequence item : items) {
if ((buff.length() + maxWidth) > width) {
println(buff);
buff.setLength(0);
if (--showLines == 0) {
// Overflow
print(resources.getString("DISPLAY_MORE"));
flush();
int c = readCharacter();
if (c == '\r' || c == '\n') {
// one step forward
showLines = 1;
}
else if (c != 'q') {
// page forward
showLines = height - 1;
}
back(resources.getString("DISPLAY_MORE").length());
if (c == 'q') {
// cancel
break;
}
}
}
// NOTE: toString() is important here due to AnsiString being retarded
buff.append(item.toString());
for (int i = 0; i < (maxWidth - item.length()); i++) {
buff.append(' ');
}
}
if (buff.length() > 0) {
println(buff);
}
}
//
// Non-supported Terminal Support
//
private Thread maskThread;
private void beforeReadLine(final String prompt, final Character mask) {
if (mask != null && maskThread == null) {
final String fullPrompt = "\r" + prompt
+ " "
+ " "
+ " "
+ "\r" + prompt;
maskThread = new Thread()
{
public void run() {
while (!interrupted()) {
try {
Writer out = getOutput();
out.write(fullPrompt);
out.flush();
sleep(3);
}
catch (IOException e) {
return;
}
catch (InterruptedException e) {
return;
}
}
}
};
maskThread.setPriority(Thread.MAX_PRIORITY);
maskThread.setDaemon(true);
maskThread.start();
}
}
private void afterReadLine() {
if (maskThread != null && maskThread.isAlive()) {
maskThread.interrupt();
}
maskThread = null;
}
/**
* Erases the current line with the existing prompt, then redraws the line
* with the provided prompt and buffer
* @param prompt
* the new prompt
* @param buffer
* the buffer to be drawn
* @param cursorDest
* where you want the cursor set when the line has been drawn.
* -1 for end of line.
* */
public void resetPromptLine(String prompt, String buffer, int cursorDest) throws IOException {
// move cursor to end of line
moveToEnd();
// backspace all text, including prompt
buf.buffer.append(this.prompt);
int promptLength = 0;
if (this.prompt != null) {
promptLength = this.prompt.length();
}
buf.cursor += promptLength;
setPrompt("");
backspaceAll();
setPrompt(prompt);
redrawLine();
setBuffer(buffer);
// move cursor to destination (-1 will move to end of line)
if (cursorDest < 0) cursorDest = buffer.length();
setCursorPosition(cursorDest);
flush();
}
public void printSearchStatus(String searchTerm, String match) throws IOException {
printSearchStatus(searchTerm, match, "(reverse-i-search)`");
}
public void printForwardSearchStatus(String searchTerm, String match) throws IOException {
printSearchStatus(searchTerm, match, "(i-search)`");
}
private void printSearchStatus(String searchTerm, String match, String searchLabel) throws IOException {
String prompt = searchLabel + searchTerm + "': ";
int cursorDest = match.indexOf(searchTerm);
resetPromptLine(prompt, match, cursorDest);
}
public void restoreLine(String originalPrompt, int cursorDest) throws IOException {
// TODO move cursor to matched string
String prompt = lastLine(originalPrompt);
String buffer = buf.buffer.toString();
resetPromptLine(prompt, buffer, cursorDest);
}
//
// History search
//
/**
* Search backward in history from a given position.
*
* @param searchTerm substring to search for.
* @param startIndex the index from which on to search
* @return index where this substring has been found, or -1 else.
*/
public int searchBackwards(String searchTerm, int startIndex) {
return searchBackwards(searchTerm, startIndex, false);
}
/**
* Search backwards in history from the current position.
*
* @param searchTerm substring to search for.
* @return index where the substring has been found, or -1 else.
*/
public int searchBackwards(String searchTerm) {
return searchBackwards(searchTerm, history.index());
}
public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) {
ListIterator it = history.entries(startIndex);
while (it.hasPrevious()) {
History.Entry e = it.previous();
if (startsWith) {
if (e.value().toString().startsWith(searchTerm)) {
return e.index();
}
} else {
if (e.value().toString().contains(searchTerm)) {
return e.index();
}
}
}
return -1;
}
/**
* Search forward in history from a given position.
*
* @param searchTerm substring to search for.
* @param startIndex the index from which on to search
* @return index where this substring has been found, or -1 else.
*/
public int searchForwards(String searchTerm, int startIndex) {
return searchForwards(searchTerm, startIndex, false);
}
/**
* Search forwards in history from the current position.
*
* @param searchTerm substring to search for.
* @return index where the substring has been found, or -1 else.
*/
public int searchForwards(String searchTerm) {
return searchForwards(searchTerm, history.index());
}
public int searchForwards(String searchTerm, int startIndex, boolean startsWith) {
ListIterator it = history.entries(startIndex);
if (searchIndex != -1 && it.hasNext()) {
it.next();
}
while (it.hasNext()) {
History.Entry e = it.next();
if (startsWith) {
if (e.value().toString().startsWith(searchTerm)) {
return e.index();
}
} else {
if (e.value().toString().contains(searchTerm)) {
return e.index();
}
}
}
return -1;
}
//
// Helpers
//
/**
* Checks to see if the specified character is a delimiter. We consider a
* character a delimiter if it is anything but a letter or digit.
*
* @param c The character to test
* @return True if it is a delimiter
*/
private boolean isDelimiter(final char c) {
return !Character.isLetterOrDigit(c);
}
/**
* Checks to see if a character is a whitespace character. Currently
* this delegates to {@link Character#isWhitespace(char)}, however
* eventually it should be hooked up so that the definition of whitespace
* can be configured, as readline does.
*
* @param c The character to check
* @return true if the character is a whitespace
*/
private boolean isWhitespace(final char c) {
return Character.isWhitespace (c);
}
private void printAnsiSequence(String sequence) throws IOException {
print(27);
print('[');
print(sequence);
flush(); // helps with step debugging
}
}
src/main/java/jline/console/CursorBuffer.java 0000664 0000000 0000000 00000005307 12146220444 0021533 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console;
import static jline.internal.Preconditions.checkNotNull;
/**
* A holder for a {@link StringBuilder} that also contains the current cursor position.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.0
*/
public class CursorBuffer
{
private boolean overTyping = false;
public int cursor = 0;
public final StringBuilder buffer = new StringBuilder();
public CursorBuffer copy () {
CursorBuffer that = new CursorBuffer();
that.overTyping = this.overTyping;
that.cursor = this.cursor;
that.buffer.append (this.toString());
return that;
}
public boolean isOverTyping() {
return overTyping;
}
public void setOverTyping(final boolean b) {
overTyping = b;
}
public int length() {
return buffer.length();
}
public char nextChar() {
if (cursor == buffer.length()) {
return 0;
} else {
return buffer.charAt(cursor);
}
}
public char current() {
if (cursor <= 0) {
return 0;
}
return buffer.charAt(cursor - 1);
}
/**
* Write the specific character into the buffer, setting the cursor position
* ahead one. The text may overwrite or insert based on the current setting
* of {@link #isOverTyping}.
*
* @param c the character to insert
*/
public void write(final char c) {
buffer.insert(cursor++, c);
if (isOverTyping() && cursor < buffer.length()) {
buffer.deleteCharAt(cursor);
}
}
/**
* Insert the specified chars into the buffer, setting the cursor to the end of the insertion point.
*/
public void write(final CharSequence str) {
checkNotNull(str);
if (buffer.length() == 0) {
buffer.append(str);
}
else {
buffer.insert(cursor, str);
}
cursor += str.length();
if (isOverTyping() && cursor < buffer.length()) {
buffer.delete(cursor, (cursor + str.length()));
}
}
public boolean clear() {
if (buffer.length() == 0) {
return false;
}
buffer.delete(0, buffer.length());
cursor = 0;
return true;
}
@Override
public String toString() {
return buffer.toString();
}
}
src/main/java/jline/console/KeyMap.java 0000664 0000000 0000000 00000066572 12146220444 0020325 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console;
import java.util.HashMap;
import java.util.Map;
/**
* The KeyMap class contains all bindings from keys to operations.
*
* @author Guillaume Nodet
* @since 2.6
*/
public class KeyMap {
public static final String VI_MOVE = "vi-move";
public static final String VI_INSERT = "vi-insert";
public static final String EMACS = "emacs";
public static final String EMACS_STANDARD = "emacs-standard";
public static final String EMACS_CTLX = "emacs-ctlx";
public static final String EMACS_META = "emacs-meta";
private static final int KEYMAP_LENGTH = 256;
private static final Object NULL_FUNCTION = new Object();
private Object[] mapping = new Object[KEYMAP_LENGTH];
private Object anotherKey = null;
private String name;
private boolean isViKeyMap;
public KeyMap(String name, boolean isViKeyMap) {
this(name, new Object[KEYMAP_LENGTH], isViKeyMap);
}
protected KeyMap(String name, Object[] mapping, boolean isViKeyMap) {
this.mapping = mapping;
this.name = name;
this.isViKeyMap = isViKeyMap;
}
public boolean isViKeyMap() {
return isViKeyMap;
}
public String getName() {
return name;
}
public Object getAnotherKey() {
return anotherKey;
}
public void from(KeyMap other) {
this.mapping = other.mapping;
this.anotherKey = other.anotherKey;
}
public Object getBound( CharSequence keySeq ) {
if (keySeq != null && keySeq.length() > 0) {
KeyMap map = this;
for (int i = 0; i < keySeq.length(); i++) {
char c = keySeq.charAt(i);
if (c > 255) {
return Operation.SELF_INSERT;
}
if (map.mapping[c] instanceof KeyMap) {
if (i == keySeq.length() - 1) {
return map.mapping[c];
} else {
map = (KeyMap) map.mapping[c];
}
} else {
return map.mapping[c];
}
}
}
return null;
}
public void bindIfNotBound( CharSequence keySeq, Object function ) {
bind (this, keySeq, function, true);
}
public void bind( CharSequence keySeq, Object function ) {
bind (this, keySeq, function, false);
}
private static void bind( KeyMap map, CharSequence keySeq, Object function ) {
bind (map, keySeq, function, false);
}
private static void bind( KeyMap map, CharSequence keySeq, Object function,
boolean onlyIfNotBound ) {
if (keySeq != null && keySeq.length() > 0) {
for (int i = 0; i < keySeq.length(); i++) {
char c = keySeq.charAt(i);
if (c >= map.mapping.length) {
return;
}
if (i < keySeq.length() - 1) {
if (!(map.mapping[c] instanceof KeyMap)) {
KeyMap m = new KeyMap("anonymous", false);
if (map.mapping[c] != Operation.DO_LOWERCASE_VERSION) {
m.anotherKey = map.mapping[c];
}
map.mapping[c] = m;
}
map = (KeyMap) map.mapping[c];
} else {
if (function == null) {
function = NULL_FUNCTION;
}
if (map.mapping[c] instanceof KeyMap) {
map.anotherKey = function;
} else {
Object op = map.mapping[c];
if (onlyIfNotBound == false
|| op == null
|| op == Operation.DO_LOWERCASE_VERSION
|| op == Operation.VI_MOVEMENT_MODE ) {
}
map.mapping[c] = function;
}
}
}
}
}
public void setBlinkMatchingParen(boolean on) {
if (on) {
bind( "}", Operation.INSERT_CLOSE_CURLY );
bind( ")", Operation.INSERT_CLOSE_PAREN );
bind( "]", Operation.INSERT_CLOSE_SQUARE );
}
}
private static void bindArrowKeys(KeyMap map) {
// MS-DOS
bind( map, "\033[0A", Operation.PREVIOUS_HISTORY );
bind( map, "\033[0B", Operation.BACKWARD_CHAR );
bind( map, "\033[0C", Operation.FORWARD_CHAR );
bind( map, "\033[0D", Operation.NEXT_HISTORY );
// Windows
bind( map, "\340\000", Operation.KILL_WHOLE_LINE );
bind( map, "\340\107", Operation.BEGINNING_OF_LINE );
bind( map, "\340\110", Operation.PREVIOUS_HISTORY );
bind( map, "\340\111", Operation.BEGINNING_OF_HISTORY );
bind( map, "\340\113", Operation.BACKWARD_CHAR );
bind( map, "\340\115", Operation.FORWARD_CHAR );
bind( map, "\340\117", Operation.END_OF_LINE );
bind( map, "\340\120", Operation.NEXT_HISTORY );
bind( map, "\340\121", Operation.END_OF_HISTORY );
bind( map, "\340\122", Operation.OVERWRITE_MODE );
bind( map, "\340\123", Operation.DELETE_CHAR );
bind( map, "\000\107", Operation.BEGINNING_OF_LINE );
bind( map, "\000\110", Operation.PREVIOUS_HISTORY );
bind( map, "\000\111", Operation.BEGINNING_OF_HISTORY );
bind( map, "\000\110", Operation.PREVIOUS_HISTORY );
bind( map, "\000\113", Operation.BACKWARD_CHAR );
bind( map, "\000\115", Operation.FORWARD_CHAR );
bind( map, "\000\117", Operation.END_OF_LINE );
bind( map, "\000\120", Operation.NEXT_HISTORY );
bind( map, "\000\121", Operation.END_OF_HISTORY );
bind( map, "\000\122", Operation.OVERWRITE_MODE );
bind( map, "\000\123", Operation.DELETE_CHAR );
bind( map, "\033[A", Operation.PREVIOUS_HISTORY );
bind( map, "\033[B", Operation.NEXT_HISTORY );
bind( map, "\033[C", Operation.FORWARD_CHAR );
bind( map, "\033[D", Operation.BACKWARD_CHAR );
bind( map, "\033[H", Operation.BEGINNING_OF_LINE );
bind( map, "\033[F", Operation.END_OF_LINE );
bind( map, "\033OA", Operation.PREVIOUS_HISTORY );
bind( map, "\033OB", Operation.NEXT_HISTORY );
bind( map, "\033OC", Operation.FORWARD_CHAR );
bind( map, "\033OD", Operation.BACKWARD_CHAR );
bind( map, "\033OH", Operation.BEGINNING_OF_LINE );
bind( map, "\033OF", Operation.END_OF_LINE );
bind( map, "\033[3~", Operation.DELETE_CHAR);
// MINGW32
bind( map, "\0340H", Operation.PREVIOUS_HISTORY );
bind( map, "\0340P", Operation.NEXT_HISTORY );
bind( map, "\0340M", Operation.FORWARD_CHAR );
bind( map, "\0340K", Operation.BACKWARD_CHAR );
}
// public boolean isConvertMetaCharsToAscii() {
// return convertMetaCharsToAscii;
// }
// public void setConvertMetaCharsToAscii(boolean convertMetaCharsToAscii) {
// this.convertMetaCharsToAscii = convertMetaCharsToAscii;
// }
public static boolean isMeta( char c ) {
return c > 0x7f && c <= 0xff;
}
public static char unMeta( char c ) {
return (char) (c & 0x7F);
}
public static char meta( char c ) {
return (char) (c | 0x80);
}
public static Map keyMaps() {
Map keyMaps = new HashMap();
KeyMap emacs = emacs();
bindArrowKeys(emacs);
keyMaps.put(EMACS, emacs);
keyMaps.put(EMACS_STANDARD, emacs);
keyMaps.put(EMACS_CTLX, (KeyMap) emacs.getBound("\u0018"));
keyMaps.put(EMACS_META, (KeyMap) emacs.getBound("\u001b"));
KeyMap viMov = viMovement();
bindArrowKeys(viMov);
keyMaps.put(VI_MOVE, viMov);
keyMaps.put("vi-command", viMov);
KeyMap viIns = viInsertion();
bindArrowKeys(viIns);
keyMaps.put(VI_INSERT, viIns);
keyMaps.put("vi", viIns);
return keyMaps;
}
public static KeyMap emacs() {
Object[] map = new Object[KEYMAP_LENGTH];
Object[] ctrl = new Object[] {
// Control keys.
Operation.SET_MARK, /* Control-@ */
Operation.BEGINNING_OF_LINE, /* Control-A */
Operation.BACKWARD_CHAR, /* Control-B */
Operation.INTERRUPT, /* Control-C */
Operation.EXIT_OR_DELETE_CHAR, /* Control-D */
Operation.END_OF_LINE, /* Control-E */
Operation.FORWARD_CHAR, /* Control-F */
Operation.ABORT, /* Control-G */
Operation.BACKWARD_DELETE_CHAR, /* Control-H */
Operation.COMPLETE, /* Control-I */
Operation.ACCEPT_LINE, /* Control-J */
Operation.KILL_LINE, /* Control-K */
Operation.CLEAR_SCREEN, /* Control-L */
Operation.ACCEPT_LINE, /* Control-M */
Operation.NEXT_HISTORY, /* Control-N */
null, /* Control-O */
Operation.PREVIOUS_HISTORY, /* Control-P */
Operation.QUOTED_INSERT, /* Control-Q */
Operation.REVERSE_SEARCH_HISTORY, /* Control-R */
Operation.FORWARD_SEARCH_HISTORY, /* Control-S */
Operation.TRANSPOSE_CHARS, /* Control-T */
Operation.UNIX_LINE_DISCARD, /* Control-U */
Operation.QUOTED_INSERT, /* Control-V */
Operation.UNIX_WORD_RUBOUT, /* Control-W */
emacsCtrlX(), /* Control-X */
Operation.YANK, /* Control-Y */
null, /* Control-Z */
emacsMeta(), /* Control-[ */
null, /* Control-\ */
Operation.CHARACTER_SEARCH, /* Control-] */
null, /* Control-^ */
Operation.UNDO, /* Control-_ */
};
System.arraycopy( ctrl, 0, map, 0, ctrl.length );
for (int i = 32; i < 256; i++) {
map[i] = Operation.SELF_INSERT;
}
map[DELETE] = Operation.BACKWARD_DELETE_CHAR;
return new KeyMap(EMACS, map, false);
}
public static final char CTRL_D = (char) 4;
public static final char CTRL_G = (char) 7;
public static final char CTRL_H = (char) 8;
public static final char CTRL_I = (char) 9;
public static final char CTRL_J = (char) 10;
public static final char CTRL_M = (char) 13;
public static final char CTRL_R = (char) 18;
public static final char CTRL_S = (char) 19;
public static final char CTRL_U = (char) 21;
public static final char CTRL_X = (char) 24;
public static final char CTRL_Y = (char) 25;
public static final char ESCAPE = (char) 27; /* Ctrl-[ */
public static final char CTRL_OB = (char) 27; /* Ctrl-[ */
public static final char CTRL_CB = (char) 29; /* Ctrl-] */
public static final int DELETE = (char) 127;
public static KeyMap emacsCtrlX() {
Object[] map = new Object[KEYMAP_LENGTH];
map[CTRL_G] = Operation.ABORT;
map[CTRL_R] = Operation.RE_READ_INIT_FILE;
map[CTRL_U] = Operation.UNDO;
map[CTRL_X] = Operation.EXCHANGE_POINT_AND_MARK;
map['('] = Operation.START_KBD_MACRO;
map[')'] = Operation.END_KBD_MACRO;
for (int i = 'A'; i <= 'Z'; i++) {
map[i] = Operation.DO_LOWERCASE_VERSION;
}
map['e'] = Operation.CALL_LAST_KBD_MACRO;
map[DELETE] = Operation.KILL_LINE;
return new KeyMap(EMACS_CTLX, map, false);
}
public static KeyMap emacsMeta() {
Object[] map = new Object[KEYMAP_LENGTH];
map[CTRL_G] = Operation.ABORT;
map[CTRL_H] = Operation.BACKWARD_KILL_WORD;
map[CTRL_I] = Operation.TAB_INSERT;
map[CTRL_J] = Operation.VI_EDITING_MODE;
map[CTRL_M] = Operation.VI_EDITING_MODE;
map[CTRL_R] = Operation.REVERT_LINE;
map[CTRL_Y] = Operation.YANK_NTH_ARG;
map[CTRL_OB] = Operation.COMPLETE;
map[CTRL_CB] = Operation.CHARACTER_SEARCH_BACKWARD;
map[' '] = Operation.SET_MARK;
map['#'] = Operation.INSERT_COMMENT;
map['&'] = Operation.TILDE_EXPAND;
map['*'] = Operation.INSERT_COMPLETIONS;
map['-'] = Operation.DIGIT_ARGUMENT;
map['.'] = Operation.YANK_LAST_ARG;
map['<'] = Operation.BEGINNING_OF_HISTORY;
map['='] = Operation.POSSIBLE_COMPLETIONS;
map['>'] = Operation.END_OF_HISTORY;
map['?'] = Operation.POSSIBLE_COMPLETIONS;
for (int i = 'A'; i <= 'Z'; i++) {
map[i] = Operation.DO_LOWERCASE_VERSION;
}
map['\\'] = Operation.DELETE_HORIZONTAL_SPACE;
map['_'] = Operation.YANK_LAST_ARG;
map['b'] = Operation.BACKWARD_WORD;
map['c'] = Operation.CAPITALIZE_WORD;
map['d'] = Operation.KILL_WORD;
map['f'] = Operation.FORWARD_WORD;
map['l'] = Operation.DOWNCASE_WORD;
map['p'] = Operation.NON_INCREMENTAL_REVERSE_SEARCH_HISTORY;
map['r'] = Operation.REVERT_LINE;
map['t'] = Operation.TRANSPOSE_WORDS;
map['u'] = Operation.UPCASE_WORD;
map['y'] = Operation.YANK_POP;
map['~'] = Operation.TILDE_EXPAND;
map[DELETE] = Operation.BACKWARD_KILL_WORD;
return new KeyMap(EMACS_META, map, false);
}
public static KeyMap viInsertion() {
Object[] map = new Object[KEYMAP_LENGTH];
Object[] ctrl = new Object[] {
// Control keys.
null, /* Control-@ */
Operation.SELF_INSERT, /* Control-A */
Operation.SELF_INSERT, /* Control-B */
Operation.SELF_INSERT, /* Control-C */
Operation.VI_EOF_MAYBE, /* Control-D */
Operation.SELF_INSERT, /* Control-E */
Operation.SELF_INSERT, /* Control-F */
Operation.SELF_INSERT, /* Control-G */
Operation.BACKWARD_DELETE_CHAR, /* Control-H */
Operation.COMPLETE, /* Control-I */
Operation.ACCEPT_LINE, /* Control-J */
Operation.SELF_INSERT, /* Control-K */
Operation.SELF_INSERT, /* Control-L */
Operation.ACCEPT_LINE, /* Control-M */
Operation.MENU_COMPLETE, /* Control-N */
Operation.SELF_INSERT, /* Control-O */
Operation.MENU_COMPLETE_BACKWARD, /* Control-P */
Operation.SELF_INSERT, /* Control-Q */
Operation.REVERSE_SEARCH_HISTORY, /* Control-R */
Operation.FORWARD_SEARCH_HISTORY, /* Control-S */
Operation.TRANSPOSE_CHARS, /* Control-T */
Operation.UNIX_LINE_DISCARD, /* Control-U */
Operation.QUOTED_INSERT, /* Control-V */
Operation.UNIX_WORD_RUBOUT, /* Control-W */
Operation.SELF_INSERT, /* Control-X */
Operation.YANK, /* Control-Y */
Operation.SELF_INSERT, /* Control-Z */
Operation.VI_MOVEMENT_MODE, /* Control-[ */
Operation.SELF_INSERT, /* Control-\ */
Operation.SELF_INSERT, /* Control-] */
Operation.SELF_INSERT, /* Control-^ */
Operation.UNDO, /* Control-_ */
};
System.arraycopy( ctrl, 0, map, 0, ctrl.length );
for (int i = 32; i < 256; i++) {
map[i] = Operation.SELF_INSERT;
}
map[DELETE] = Operation.BACKWARD_DELETE_CHAR;
return new KeyMap(VI_INSERT, map, false);
}
public static KeyMap viMovement() {
Object[] map = new Object[KEYMAP_LENGTH];
Object[] low = new Object[] {
// Control keys.
null, /* Control-@ */
null, /* Control-A */
null, /* Control-B */
Operation.INTERRUPT, /* Control-C */
/*
* ^D is supposed to move down half a screen. In bash
* appears to be ignored.
*/
Operation.VI_EOF_MAYBE, /* Control-D */
Operation.EMACS_EDITING_MODE, /* Control-E */
null, /* Control-F */
Operation.ABORT, /* Control-G */
Operation.BACKWARD_CHAR, /* Control-H */
null, /* Control-I */
Operation.VI_MOVE_ACCEPT_LINE, /* Control-J */
Operation.KILL_LINE, /* Control-K */
Operation.CLEAR_SCREEN, /* Control-L */
Operation.VI_MOVE_ACCEPT_LINE, /* Control-M */
Operation.VI_NEXT_HISTORY, /* Control-N */
null, /* Control-O */
Operation.VI_PREVIOUS_HISTORY, /* Control-P */
/*
* My testing with readline is the ^Q is ignored.
* Maybe this should be null?
*/
Operation.QUOTED_INSERT, /* Control-Q */
/*
* TODO - Very broken. While in forward/reverse
* history search the VI keyset should go out the
* window and we need to enter a very simple keymap.
*/
Operation.REVERSE_SEARCH_HISTORY, /* Control-R */
/* TODO */
Operation.FORWARD_SEARCH_HISTORY, /* Control-S */
Operation.TRANSPOSE_CHARS, /* Control-T */
Operation.UNIX_LINE_DISCARD, /* Control-U */
/* TODO */
Operation.QUOTED_INSERT, /* Control-V */
Operation.UNIX_WORD_RUBOUT, /* Control-W */
null, /* Control-X */
/* TODO */
Operation.YANK, /* Control-Y */
null, /* Control-Z */
emacsMeta(), /* Control-[ */
null, /* Control-\ */
/* TODO */
Operation.CHARACTER_SEARCH, /* Control-] */
null, /* Control-^ */
/* TODO */
Operation.UNDO, /* Control-_ */
Operation.FORWARD_CHAR, /* SPACE */
null, /* ! */
null, /* " */
Operation.VI_INSERT_COMMENT, /* # */
Operation.END_OF_LINE, /* $ */
Operation.VI_MATCH, /* % */
Operation.VI_TILDE_EXPAND, /* & */
null, /* ' */
null, /* ( */
null, /* ) */
/* TODO */
Operation.VI_COMPLETE, /* * */
Operation.VI_NEXT_HISTORY, /* + */
Operation.VI_CHAR_SEARCH, /* , */
Operation.VI_PREVIOUS_HISTORY, /* - */
/* TODO */
Operation.VI_REDO, /* . */
Operation.VI_SEARCH, /* / */
Operation.VI_BEGNNING_OF_LINE_OR_ARG_DIGIT, /* 0 */
Operation.VI_ARG_DIGIT, /* 1 */
Operation.VI_ARG_DIGIT, /* 2 */
Operation.VI_ARG_DIGIT, /* 3 */
Operation.VI_ARG_DIGIT, /* 4 */
Operation.VI_ARG_DIGIT, /* 5 */
Operation.VI_ARG_DIGIT, /* 6 */
Operation.VI_ARG_DIGIT, /* 7 */
Operation.VI_ARG_DIGIT, /* 8 */
Operation.VI_ARG_DIGIT, /* 9 */
null, /* : */
Operation.VI_CHAR_SEARCH, /* ; */
null, /* < */
Operation.VI_COMPLETE, /* = */
null, /* > */
Operation.VI_SEARCH, /* ? */
null, /* @ */
Operation.VI_APPEND_EOL, /* A */
Operation.VI_PREV_WORD, /* B */
Operation.VI_CHANGE_TO, /* C */
Operation.VI_DELETE_TO, /* D */
Operation.VI_END_WORD, /* E */
Operation.VI_CHAR_SEARCH, /* F */
/* I need to read up on what this does */
Operation.VI_FETCH_HISTORY, /* G */
null, /* H */
Operation.VI_INSERT_BEG, /* I */
null, /* J */
null, /* K */
null, /* L */
null, /* M */
Operation.VI_SEARCH_AGAIN, /* N */
null, /* O */
Operation.VI_PUT, /* P */
null, /* Q */
/* TODO */
Operation.VI_REPLACE, /* R */
Operation.VI_SUBST, /* S */
Operation.VI_CHAR_SEARCH, /* T */
/* TODO */
Operation.REVERT_LINE, /* U */
null, /* V */
Operation.VI_NEXT_WORD, /* W */
Operation.VI_RUBOUT, /* X */
Operation.VI_YANK_TO, /* Y */
null, /* Z */
null, /* [ */
Operation.VI_COMPLETE, /* \ */
null, /* ] */
Operation.VI_FIRST_PRINT, /* ^ */
Operation.VI_YANK_ARG, /* _ */
Operation.VI_GOTO_MARK, /* ` */
Operation.VI_APPEND_MODE, /* a */
Operation.VI_PREV_WORD, /* b */
Operation.VI_CHANGE_TO, /* c */
Operation.VI_DELETE_TO, /* d */
Operation.VI_END_WORD, /* e */
Operation.VI_CHAR_SEARCH, /* f */
null, /* g */
Operation.BACKWARD_CHAR, /* h */
Operation.VI_INSERTION_MODE, /* i */
Operation.NEXT_HISTORY, /* j */
Operation.PREVIOUS_HISTORY, /* k */
Operation.FORWARD_CHAR, /* l */
Operation.VI_SET_MARK, /* m */
Operation.VI_SEARCH_AGAIN, /* n */
null, /* o */
Operation.VI_PUT, /* p */
null, /* q */
Operation.VI_CHANGE_CHAR, /* r */
Operation.VI_SUBST, /* s */
Operation.VI_CHAR_SEARCH, /* t */
Operation.UNDO, /* u */
null, /* v */
Operation.VI_NEXT_WORD, /* w */
Operation.VI_DELETE, /* x */
Operation.VI_YANK_TO, /* y */
null, /* z */
null, /* { */
Operation.VI_COLUMN, /* | */
null, /* } */
Operation.VI_CHANGE_CASE, /* ~ */
Operation.VI_DELETE /* DEL */
};
System.arraycopy( low, 0, map, 0, low.length );
for (int i = 128; i < 256; i++) {
map[i] = null;
}
return new KeyMap(VI_MOVE, map, false);
}
}
src/main/java/jline/console/Operation.java 0000664 0000000 0000000 00000006444 12146220444 0021067 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console;
/**
* List of all operations.
*
* @author Guillaume Nodet
* @since 2.6
*/
public enum Operation {
ABORT,
ACCEPT_LINE,
ARROW_KEY_PREFIX,
BACKWARD_BYTE,
BACKWARD_CHAR,
BACKWARD_DELETE_CHAR,
BACKWARD_KILL_LINE,
BACKWARD_KILL_WORD,
BACKWARD_WORD,
BEGINNING_OF_HISTORY,
BEGINNING_OF_LINE,
CALL_LAST_KBD_MACRO,
CAPITALIZE_WORD,
CHARACTER_SEARCH,
CHARACTER_SEARCH_BACKWARD,
CLEAR_SCREEN,
COMPLETE,
COPY_BACKWARD_WORD,
COPY_FORWARD_WORD,
COPY_REGION_AS_KILL,
DELETE_CHAR,
DELETE_CHAR_OR_LIST,
DELETE_HORIZONTAL_SPACE,
DIGIT_ARGUMENT,
DO_LOWERCASE_VERSION,
DOWNCASE_WORD,
DUMP_FUNCTIONS,
DUMP_MACROS,
DUMP_VARIABLES,
EMACS_EDITING_MODE,
END_KBD_MACRO,
END_OF_HISTORY,
END_OF_LINE,
EXCHANGE_POINT_AND_MARK,
EXIT_OR_DELETE_CHAR,
FORWARD_BACKWARD_DELETE_CHAR,
FORWARD_BYTE,
FORWARD_CHAR,
FORWARD_SEARCH_HISTORY,
FORWARD_WORD,
HISTORY_SEARCH_BACKWARD,
HISTORY_SEARCH_FORWARD,
INSERT_CLOSE_CURLY,
INSERT_CLOSE_PAREN,
INSERT_CLOSE_SQUARE,
INSERT_COMMENT,
INSERT_COMPLETIONS,
INTERRUPT,
KILL_WHOLE_LINE,
KILL_LINE,
KILL_REGION,
KILL_WORD,
MENU_COMPLETE,
MENU_COMPLETE_BACKWARD,
NEXT_HISTORY,
NON_INCREMENTAL_FORWARD_SEARCH_HISTORY,
NON_INCREMENTAL_REVERSE_SEARCH_HISTORY,
NON_INCREMENTAL_FORWARD_SEARCH_HISTORY_AGAIN,
NON_INCREMENTAL_REVERSE_SEARCH_HISTORY_AGAIN,
OLD_MENU_COMPLETE,
OVERWRITE_MODE,
PASTE_FROM_CLIPBOARD,
POSSIBLE_COMPLETIONS,
PREVIOUS_HISTORY,
QUOTED_INSERT,
RE_READ_INIT_FILE,
REDRAW_CURRENT_LINE,
REVERSE_SEARCH_HISTORY,
REVERT_LINE,
SELF_INSERT,
SET_MARK,
SKIP_CSI_SEQUENCE,
START_KBD_MACRO,
TAB_INSERT,
TILDE_EXPAND,
TRANSPOSE_CHARS,
TRANSPOSE_WORDS,
TTY_STATUS,
UNDO,
UNIVERSAL_ARGUMENT,
UNIX_FILENAME_RUBOUT,
UNIX_LINE_DISCARD,
UNIX_WORD_RUBOUT,
UPCASE_WORD,
YANK,
YANK_LAST_ARG,
YANK_NTH_ARG,
YANK_POP,
VI_APPEND_EOL,
VI_APPEND_MODE,
VI_ARG_DIGIT,
VI_BACK_TO_INDENT,
VI_BACKWARD_BIGWORD,
VI_BACKWARD_WORD,
VI_BWORD,
VI_CHANGE_CASE,
VI_CHANGE_CHAR,
VI_CHANGE_TO,
VI_CHAR_SEARCH,
VI_COLUMN,
VI_COMPLETE,
VI_DELETE,
VI_DELETE_TO,
VI_EDITING_MODE,
VI_END_BIGWORD,
VI_END_WORD,
VI_EOF_MAYBE,
VI_EWORD,
VI_FWORD,
VI_FETCH_HISTORY,
VI_FIRST_PRINT,
VI_FORWARD_BIGWORD,
VI_FORWARD_WORD,
VI_GOTO_MARK,
VI_INSERT_BEG,
VI_INSERTION_MODE,
VI_MATCH,
VI_MOVEMENT_MODE,
VI_NEXT_WORD,
VI_OVERSTRIKE,
VI_OVERSTRIKE_DELETE,
VI_PREV_WORD,
VI_PUT,
VI_REDO,
VI_REPLACE,
VI_RUBOUT,
VI_SEARCH,
VI_SEARCH_AGAIN,
VI_SET_MARK,
VI_SUBST,
VI_TILDE_EXPAND,
VI_YANK_ARG,
VI_YANK_TO,
VI_MOVE_ACCEPT_LINE,
VI_NEXT_HISTORY,
VI_PREVIOUS_HISTORY,
VI_INSERT_COMMENT,
VI_BEGNNING_OF_LINE_OR_ARG_DIGIT,
}
src/main/java/jline/console/UserInterruptException.java 0000664 0000000 0000000 00000001630 12146220444 0023631 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console;
/**
* This exception is thrown by {@link ConsoleReader#readLine} when
* user interrupt handling is enabled and the user types the
* interrupt character (ctrl-C). The partially entered line is
* available via the {@link #getPartialLine()} method.
*/
public class UserInterruptException
extends RuntimeException
{
private final String partialLine;
public UserInterruptException(String partialLine)
{
this.partialLine = partialLine;
}
/**
* @return the partially entered line when ctrl-C was pressed
*/
public String getPartialLine()
{
return partialLine;
}
}
src/main/java/jline/console/completer/ 0000775 0000000 0000000 00000000000 12146220444 0020246 5 ustar 00root root 0000000 0000000 src/main/java/jline/console/completer/AggregateCompleter.java 0000664 0000000 0000000 00000007205 12146220444 0024656 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import static jline.internal.Preconditions.checkNotNull;
/**
* Completer which contains multiple completers and aggregates them together.
*
* @author Jason Dillon
* @since 2.3
*/
public class AggregateCompleter
implements Completer
{
private final List completers = new ArrayList();
public AggregateCompleter() {
// empty
}
/**
* Construct an AggregateCompleter with the given collection of completers.
* The completers will be used in the iteration order of the collection.
*
* @param completers the collection of completers
*/
public AggregateCompleter(final Collection completers) {
checkNotNull(completers);
this.completers.addAll(completers);
}
/**
* Construct an AggregateCompleter with the given completers.
* The completers will be used in the order given.
*
* @param completers the completers
*/
public AggregateCompleter(final Completer... completers) {
this(Arrays.asList(completers));
}
/**
* Retrieve the collection of completers currently being aggregated.
*
* @return the aggregated completers
*/
public Collection getCompleters() {
return completers;
}
/**
* Perform a completion operation across all aggregated completers.
*
* @see Completer#complete(String, int, java.util.List)
* @return the highest completion return value from all completers
*/
public int complete(final String buffer, final int cursor, final List candidates) {
// buffer could be null
checkNotNull(candidates);
List completions = new ArrayList(completers.size());
// Run each completer, saving its completion results
int max = -1;
for (Completer completer : completers) {
Completion completion = new Completion(candidates);
completion.complete(completer, buffer, cursor);
// Compute the max cursor position
max = Math.max(max, completion.cursor);
completions.add(completion);
}
// Append candidates from completions which have the same cursor position as max
for (Completion completion : completions) {
if (completion.cursor == max) {
candidates.addAll(completion.candidates);
}
}
return max;
}
/**
* @return a string representing the aggregated completers
*/
@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"completers=" + completers +
'}';
}
private class Completion
{
public final List candidates;
public int cursor;
public Completion(final List candidates) {
checkNotNull(candidates);
this.candidates = new LinkedList(candidates);
}
public void complete(final Completer completer, final String buffer, final int cursor) {
checkNotNull(completer);
this.cursor = completer.complete(buffer, cursor, candidates);
}
}
}
src/main/java/jline/console/completer/ArgumentCompleter.java 0000664 0000000 0000000 00000032460 12146220444 0024553 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import jline.internal.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import static jline.internal.Preconditions.checkNotNull;
/**
* A {@link Completer} implementation that invokes a child completer using the appropriate separator argument.
* This can be used instead of the individual completers having to know about argument parsing semantics.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.3
*/
public class ArgumentCompleter
implements Completer
{
private final ArgumentDelimiter delimiter;
private final List completers = new ArrayList();
private boolean strict = true;
/**
* Create a new completer with the specified argument delimiter.
*
* @param delimiter The delimiter for parsing arguments
* @param completers The embedded completers
*/
public ArgumentCompleter(final ArgumentDelimiter delimiter, final Collection completers) {
this.delimiter = checkNotNull(delimiter);
checkNotNull(completers);
this.completers.addAll(completers);
}
/**
* Create a new completer with the specified argument delimiter.
*
* @param delimiter The delimiter for parsing arguments
* @param completers The embedded completers
*/
public ArgumentCompleter(final ArgumentDelimiter delimiter, final Completer... completers) {
this(delimiter, Arrays.asList(completers));
}
/**
* Create a new completer with the default {@link WhitespaceArgumentDelimiter}.
*
* @param completers The embedded completers
*/
public ArgumentCompleter(final Completer... completers) {
this(new WhitespaceArgumentDelimiter(), completers);
}
/**
* Create a new completer with the default {@link WhitespaceArgumentDelimiter}.
*
* @param completers The embedded completers
*/
public ArgumentCompleter(final List completers) {
this(new WhitespaceArgumentDelimiter(), completers);
}
/**
* If true, a completion at argument index N will only succeed
* if all the completions from 0-(N-1) also succeed.
*/
public void setStrict(final boolean strict) {
this.strict = strict;
}
/**
* Returns whether a completion at argument index N will success
* if all the completions from arguments 0-(N-1) also succeed.
*
* @return True if strict.
* @since 2.3
*/
public boolean isStrict() {
return this.strict;
}
/**
* @since 2.3
*/
public ArgumentDelimiter getDelimiter() {
return delimiter;
}
/**
* @since 2.3
*/
public List getCompleters() {
return completers;
}
public int complete(final String buffer, final int cursor, final List candidates) {
// buffer can be null
checkNotNull(candidates);
ArgumentDelimiter delim = getDelimiter();
ArgumentList list = delim.delimit(buffer, cursor);
int argpos = list.getArgumentPosition();
int argIndex = list.getCursorArgumentIndex();
if (argIndex < 0) {
return -1;
}
List completers = getCompleters();
Completer completer;
// if we are beyond the end of the completers, just use the last one
if (argIndex >= completers.size()) {
completer = completers.get(completers.size() - 1);
}
else {
completer = completers.get(argIndex);
}
// ensure that all the previous completers are successful before allowing this completer to pass (only if strict).
for (int i = 0; isStrict() && (i < argIndex); i++) {
Completer sub = completers.get(i >= completers.size() ? (completers.size() - 1) : i);
String[] args = list.getArguments();
String arg = (args == null || i >= args.length) ? "" : args[i];
List subCandidates = new LinkedList();
if (sub.complete(arg, arg.length(), subCandidates) == -1) {
return -1;
}
if (subCandidates.size() == 0) {
return -1;
}
}
int ret = completer.complete(list.getCursorArgument(), argpos, candidates);
if (ret == -1) {
return -1;
}
int pos = ret + list.getBufferPosition() - argpos;
// Special case: when completing in the middle of a line, and the area under the cursor is a delimiter,
// then trim any delimiters from the candidates, since we do not need to have an extra delimiter.
//
// E.g., if we have a completion for "foo", and we enter "f bar" into the buffer, and move to after the "f"
// and hit TAB, we want "foo bar" instead of "foo bar".
if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) {
for (int i = 0; i < candidates.size(); i++) {
CharSequence val = candidates.get(i);
while (val.length() > 0 && delim.isDelimiter(val, val.length() - 1)) {
val = val.subSequence(0, val.length() - 1);
}
candidates.set(i, val);
}
}
Log.trace("Completing ", buffer, " (pos=", cursor, ") with: ", candidates, ": offset=", pos);
return pos;
}
/**
* The {@link ArgumentCompleter.ArgumentDelimiter} allows custom breaking up of a {@link String} into individual
* arguments in order to dispatch the arguments to the nested {@link Completer}.
*
* @author Marc Prud'hommeaux
*/
public static interface ArgumentDelimiter
{
/**
* Break the specified buffer into individual tokens that can be completed on their own.
*
* @param buffer The buffer to split
* @param pos The current position of the cursor in the buffer
* @return The tokens
*/
ArgumentList delimit(CharSequence buffer, int pos);
/**
* Returns true if the specified character is a whitespace parameter.
*
* @param buffer The complete command buffer
* @param pos The index of the character in the buffer
* @return True if the character should be a delimiter
*/
boolean isDelimiter(CharSequence buffer, int pos);
}
/**
* Abstract implementation of a delimiter that uses the {@link #isDelimiter} method to determine if a particular
* character should be used as a delimiter.
*
* @author Marc Prud'hommeaux
*/
public abstract static class AbstractArgumentDelimiter
implements ArgumentDelimiter
{
// TODO: handle argument quoting and escape characters
private char[] quoteChars = {'\'', '"'};
private char[] escapeChars = {'\\'};
public void setQuoteChars(final char[] chars) {
this.quoteChars = chars;
}
public char[] getQuoteChars() {
return this.quoteChars;
}
public void setEscapeChars(final char[] chars) {
this.escapeChars = chars;
}
public char[] getEscapeChars() {
return this.escapeChars;
}
public ArgumentList delimit(final CharSequence buffer, final int cursor) {
List args = new LinkedList();
StringBuilder arg = new StringBuilder();
int argpos = -1;
int bindex = -1;
for (int i = 0; (buffer != null) && (i <= buffer.length()); i++) {
// once we reach the cursor, set the
// position of the selected index
if (i == cursor) {
bindex = args.size();
// the position in the current argument is just the
// length of the current argument
argpos = arg.length();
}
if ((i == buffer.length()) || isDelimiter(buffer, i)) {
if (arg.length() > 0) {
args.add(arg.toString());
arg.setLength(0); // reset the arg
}
}
else {
arg.append(buffer.charAt(i));
}
}
return new ArgumentList(args.toArray(new String[args.size()]), bindex, argpos, cursor);
}
/**
* Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not
* escaped by any of {@link #getQuoteChars}, and is not escaped by ant of the {@link #getEscapeChars}, and
* returns true from {@link #isDelimiterChar}.
*
* @param buffer The complete command buffer
* @param pos The index of the character in the buffer
* @return True if the character should be a delimiter
*/
public boolean isDelimiter(final CharSequence buffer, final int pos) {
return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos);
}
public boolean isQuoted(final CharSequence buffer, final int pos) {
return false;
}
public boolean isEscaped(final CharSequence buffer, final int pos) {
if (pos <= 0) {
return false;
}
for (int i = 0; (escapeChars != null) && (i < escapeChars.length);
i++) {
if (buffer.charAt(pos) == escapeChars[i]) {
return !isEscaped(buffer, pos - 1); // escape escape
}
}
return false;
}
/**
* Returns true if the character at the specified position if a delimiter. This method will only be called if
* the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the
* {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead.
*/
public abstract boolean isDelimiterChar(CharSequence buffer, int pos);
}
/**
* {@link ArgumentCompleter.ArgumentDelimiter} implementation that counts all whitespace (as reported by
* {@link Character#isWhitespace}) as being a delimiter.
*
* @author Marc Prud'hommeaux
*/
public static class WhitespaceArgumentDelimiter
extends AbstractArgumentDelimiter
{
/**
* The character is a delimiter if it is whitespace, and the
* preceding character is not an escape character.
*/
@Override
public boolean isDelimiterChar(final CharSequence buffer, final int pos) {
return Character.isWhitespace(buffer.charAt(pos));
}
}
/**
* The result of a delimited buffer.
*
* @author Marc Prud'hommeaux
*/
public static class ArgumentList
{
private String[] arguments;
private int cursorArgumentIndex;
private int argumentPosition;
private int bufferPosition;
/**
* @param arguments The array of tokens
* @param cursorArgumentIndex The token index of the cursor
* @param argumentPosition The position of the cursor in the current token
* @param bufferPosition The position of the cursor in the whole buffer
*/
public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) {
this.arguments = checkNotNull(arguments);
this.cursorArgumentIndex = cursorArgumentIndex;
this.argumentPosition = argumentPosition;
this.bufferPosition = bufferPosition;
}
public void setCursorArgumentIndex(final int i) {
this.cursorArgumentIndex = i;
}
public int getCursorArgumentIndex() {
return this.cursorArgumentIndex;
}
public String getCursorArgument() {
if ((cursorArgumentIndex < 0) || (cursorArgumentIndex >= arguments.length)) {
return null;
}
return arguments[cursorArgumentIndex];
}
public void setArgumentPosition(final int pos) {
this.argumentPosition = pos;
}
public int getArgumentPosition() {
return this.argumentPosition;
}
public void setArguments(final String[] arguments) {
this.arguments = arguments;
}
public String[] getArguments() {
return this.arguments;
}
public void setBufferPosition(final int pos) {
this.bufferPosition = pos;
}
public int getBufferPosition() {
return this.bufferPosition;
}
}
}
src/main/java/jline/console/completer/CandidateListCompletionHandler.java 0000664 0000000 0000000 00000014047 12146220444 0027157 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import jline.console.ConsoleReader;
import jline.console.CursorBuffer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.ResourceBundle;
import java.util.Set;
/**
* A {@link CompletionHandler} that deals with multiple distinct completions
* by outputting the complete list of possibilities to the console. This
* mimics the behavior of the
* readline library.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.3
*/
public class CandidateListCompletionHandler
implements CompletionHandler
{
// TODO: handle quotes and escaped quotes && enable automatic escaping of whitespace
public boolean complete(final ConsoleReader reader, final List candidates, final int pos) throws
IOException
{
CursorBuffer buf = reader.getCursorBuffer();
// if there is only one completion, then fill in the buffer
if (candidates.size() == 1) {
CharSequence value = candidates.get(0);
// fail if the only candidate is the same as the current buffer
if (value.equals(buf.toString())) {
return false;
}
setBuffer(reader, value, pos);
return true;
}
else if (candidates.size() > 1) {
String value = getUnambiguousCompletions(candidates);
setBuffer(reader, value, pos);
}
printCandidates(reader, candidates);
// redraw the current console buffer
reader.drawLine();
return true;
}
public static void setBuffer(final ConsoleReader reader, final CharSequence value, final int offset) throws
IOException
{
while ((reader.getCursorBuffer().cursor > offset) && reader.backspace()) {
// empty
}
reader.putString(value);
reader.setCursorPosition(offset + value.length());
}
/**
* Print out the candidates. If the size of the candidates is greater than the
* {@link ConsoleReader#getAutoprintThreshold}, they prompt with a warning.
*
* @param candidates the list of candidates to print
*/
public static void printCandidates(final ConsoleReader reader, Collection candidates) throws
IOException
{
Set distinct = new HashSet(candidates);
if (distinct.size() > reader.getAutoprintThreshold()) {
//noinspection StringConcatenation
reader.print(Messages.DISPLAY_CANDIDATES.format(candidates.size()));
reader.flush();
int c;
String noOpt = Messages.DISPLAY_CANDIDATES_NO.format();
String yesOpt = Messages.DISPLAY_CANDIDATES_YES.format();
char[] allowed = {yesOpt.charAt(0), noOpt.charAt(0)};
while ((c = reader.readCharacter(allowed)) != -1) {
String tmp = new String(new char[]{(char) c});
if (noOpt.startsWith(tmp)) {
reader.println();
return;
}
else if (yesOpt.startsWith(tmp)) {
break;
}
else {
reader.beep();
}
}
}
// copy the values and make them distinct, without otherwise affecting the ordering. Only do it if the sizes differ.
if (distinct.size() != candidates.size()) {
Collection copy = new ArrayList();
for (CharSequence next : candidates) {
if (!copy.contains(next)) {
copy.add(next);
}
}
candidates = copy;
}
reader.println();
reader.printColumns(candidates);
}
/**
* Returns a root that matches all the {@link String} elements of the specified {@link List},
* or null if there are no commonalities. For example, if the list contains
* foobar, foobaz, foobuz, the method will return foob.
*/
private String getUnambiguousCompletions(final List candidates) {
if (candidates == null || candidates.isEmpty()) {
return null;
}
// convert to an array for speed
String[] strings = candidates.toArray(new String[candidates.size()]);
String first = strings[0];
StringBuilder candidate = new StringBuilder();
for (int i = 0; i < first.length(); i++) {
if (startsWith(first.substring(0, i + 1), strings)) {
candidate.append(first.charAt(i));
}
else {
break;
}
}
return candidate.toString();
}
/**
* @return true is all the elements of candidates start with starts
*/
private boolean startsWith(final String starts, final String[] candidates) {
for (String candidate : candidates) {
if (!candidate.startsWith(starts)) {
return false;
}
}
return true;
}
private static enum Messages
{
DISPLAY_CANDIDATES,
DISPLAY_CANDIDATES_YES,
DISPLAY_CANDIDATES_NO,;
private static final
ResourceBundle
bundle =
ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName(), Locale.getDefault());
public String format(final Object... args) {
if (bundle == null)
return "";
else
return String.format(bundle.getString(name()), args);
}
}
}
src/main/java/jline/console/completer/Completer.java 0000664 0000000 0000000 00000002472 12146220444 0023050 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import java.util.List;
/**
* A completer is the mechanism by which tab-completion candidates will be resolved.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.3
*/
public interface Completer
{
//
// FIXME: Check if we can use CharSequece for buffer?
//
/**
* Populates candidates with a list of possible completions for the buffer.
*
* The candidates list will not be sorted before being displayed to the user: thus, the
* complete method should sort the {@link List} before returning.
*
* @param buffer The buffer
* @param cursor The current position of the cursor in the buffer
* @param candidates The {@link List} of candidates to populate
* @return The index of the buffer for which the completion will be relative
*/
int complete(String buffer, int cursor, List candidates);
}
src/main/java/jline/console/completer/CompletionHandler.java 0000664 0000000 0000000 00000001364 12146220444 0024524 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import jline.console.ConsoleReader;
import java.io.IOException;
import java.util.List;
/**
* Handler for dealing with candidates for tab-completion.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.3
*/
public interface CompletionHandler
{
boolean complete(ConsoleReader reader, List candidates, int position) throws IOException;
}
src/main/java/jline/console/completer/EnumCompleter.java 0000664 0000000 0000000 00000001404 12146220444 0023667 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import static jline.internal.Preconditions.checkNotNull;
/**
* {@link Completer} for {@link Enum} names.
*
* @author Jason Dillon
* @since 2.3
*/
public class EnumCompleter
extends StringsCompleter
{
public EnumCompleter(Class extends Enum> source) {
checkNotNull(source);
for (Enum> n : source.getEnumConstants()) {
this.getStrings().add(n.name().toLowerCase());
}
}
} src/main/java/jline/console/completer/FileNameCompleter.java 0000664 0000000 0000000 00000007466 12146220444 0024461 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import jline.internal.Configuration;
import java.io.File;
import java.util.List;
import static jline.internal.Preconditions.checkNotNull;
/**
* A file name completer takes the buffer and issues a list of
* potential completions.
*
* This completer tries to behave as similar as possible to
* bash's file name completion (using GNU readline)
* with the following exceptions:
*
*
*
Candidates that are directories will end with "/"
*
Wildcard regular expressions are not evaluated or replaced
*
The "~" character can be used to represent the user's home,
* but it cannot complete to other users' homes, since java does
* not provide any way of determining that easily
*
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.3
*/
public class FileNameCompleter
implements Completer
{
// TODO: Handle files with spaces in them
private static final boolean OS_IS_WINDOWS;
static {
String os = Configuration.getOsName();
OS_IS_WINDOWS = os.contains("windows");
}
public int complete(String buffer, final int cursor, final List candidates) {
// buffer can be null
checkNotNull(candidates);
if (buffer == null) {
buffer = "";
}
if (OS_IS_WINDOWS) {
buffer = buffer.replace('/', '\\');
}
String translated = buffer;
File homeDir = getUserHome();
// Special character: ~ maps to the user's home directory
if (translated.startsWith("~" + separator())) {
translated = homeDir.getPath() + translated.substring(1);
}
else if (translated.startsWith("~")) {
translated = homeDir.getParentFile().getAbsolutePath();
}
else if (!(translated.startsWith(separator()))) {
String cwd = getUserDir().getAbsolutePath();
translated = cwd + separator() + translated;
}
File file = new File(translated);
final File dir;
if (translated.endsWith(separator())) {
dir = file;
}
else {
dir = file.getParentFile();
}
File[] entries = dir == null ? new File[0] : dir.listFiles();
return matchFiles(buffer, translated, entries, candidates);
}
protected String separator() {
return File.separator;
}
protected File getUserHome() {
return Configuration.getUserHome();
}
protected File getUserDir() {
return new File(".");
}
protected int matchFiles(final String buffer, final String translated, final File[] files, final List candidates) {
if (files == null) {
return -1;
}
int matches = 0;
// first pass: just count the matches
for (File file : files) {
if (file.getAbsolutePath().startsWith(translated)) {
matches++;
}
}
for (File file : files) {
if (file.getAbsolutePath().startsWith(translated)) {
CharSequence name = file.getName() + (matches == 1 && file.isDirectory() ? separator() : " ");
candidates.add(render(file, name).toString());
}
}
final int index = buffer.lastIndexOf(separator());
return index + separator().length();
}
protected CharSequence render(final File file, final CharSequence name) {
return name;
}
}
src/main/java/jline/console/completer/NullCompleter.java 0000664 0000000 0000000 00000001373 12146220444 0023702 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import java.util.List;
/**
* Null completer.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.3
*/
public final class NullCompleter
implements Completer
{
public static final NullCompleter INSTANCE = new NullCompleter();
public int complete(final String buffer, final int cursor, final List candidates) {
return -1;
}
} src/main/java/jline/console/completer/StringsCompleter.java 0000664 0000000 0000000 00000003335 12146220444 0024421 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.completer;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import static jline.internal.Preconditions.checkNotNull;
/**
* Completer for a set of strings.
*
* @author Jason Dillon
* @since 2.3
*/
public class StringsCompleter
implements Completer
{
private final SortedSet strings = new TreeSet();
public StringsCompleter() {
// empty
}
public StringsCompleter(final Collection strings) {
checkNotNull(strings);
getStrings().addAll(strings);
}
public StringsCompleter(final String... strings) {
this(Arrays.asList(strings));
}
public Collection getStrings() {
return strings;
}
public int complete(final String buffer, final int cursor, final List candidates) {
// buffer could be null
checkNotNull(candidates);
if (buffer == null) {
candidates.addAll(strings);
}
else {
for (String match : strings.tailSet(buffer)) {
if (!match.startsWith(buffer)) {
break;
}
candidates.add(match);
}
}
if (candidates.size() == 1) {
candidates.set(0, candidates.get(0) + " ");
}
return candidates.isEmpty() ? -1 : 0;
}
} src/main/java/jline/console/completer/package-info.java 0000664 0000000 0000000 00000000545 12146220444 0023441 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
/**
* Console completer support.
*
* @since 2.3
*/
package jline.console.completer; src/main/java/jline/console/history/ 0000775 0000000 0000000 00000000000 12146220444 0017755 5 ustar 00root root 0000000 0000000 src/main/java/jline/console/history/FileHistory.java 0000664 0000000 0000000 00000005413 12146220444 0023064 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.history;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.Flushable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import jline.internal.Log;
import static jline.internal.Preconditions.checkNotNull;
/**
* {@link History} using a file for persistent backing.
*
* Implementers should install shutdown hook to call {@link FileHistory#flush}
* to save history to disk.
*
* @author Jason Dillon
* @since 2.0
*/
public class FileHistory
extends MemoryHistory
implements PersistentHistory, Flushable
{
private final File file;
public FileHistory(final File file) throws IOException {
this.file = checkNotNull(file);
load(file);
}
public File getFile() {
return file;
}
public void load(final File file) throws IOException {
checkNotNull(file);
if (file.exists()) {
Log.trace("Loading history from: ", file);
load(new FileReader(file));
}
}
public void load(final InputStream input) throws IOException {
checkNotNull(input);
load(new InputStreamReader(input));
}
public void load(final Reader reader) throws IOException {
checkNotNull(reader);
BufferedReader input = new BufferedReader(reader);
String item;
while ((item = input.readLine()) != null) {
internalAdd(item);
}
}
public void flush() throws IOException {
Log.trace("Flushing history");
if (!file.exists()) {
File dir = file.getParentFile();
if (!dir.exists() && !dir.mkdirs()) {
Log.warn("Failed to create directory: ", dir);
}
if (!file.createNewFile()) {
Log.warn("Failed to create file: ", file);
}
}
PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream(file)));
try {
for (Entry entry : this) {
out.println(entry.value());
}
}
finally {
out.close();
}
}
public void purge() throws IOException {
Log.trace("Purging history");
clear();
if (!file.delete()) {
Log.warn("Failed to delete history file: ", file);
}
}
} src/main/java/jline/console/history/History.java 0000664 0000000 0000000 00000003722 12146220444 0022265 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.history;
import java.util.Iterator;
import java.util.ListIterator;
/**
* Console history.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.3
*/
public interface History
extends Iterable
{
int size();
boolean isEmpty();
int index();
void clear();
CharSequence get(int index);
void add(CharSequence line);
/**
* Set the history item at the given index to the given CharSequence.
*
* @param index the index of the history offset
* @param item the new item
* @since 2.7
*/
void set(int index, CharSequence item);
/**
* Remove the history element at the given index.
*
* @param i the index of the element to remove
* @return the removed element
* @since 2.7
*/
CharSequence remove(int i);
/**
* Remove the first element from history.
*
* @return the removed element
* @since 2.7
*/
CharSequence removeFirst();
/**
* Remove the last element from history
*
* @return the removed element
* @since 2.7
*/
CharSequence removeLast();
void replace(CharSequence item);
//
// Entries
//
interface Entry
{
int index();
CharSequence value();
}
ListIterator entries(int index);
ListIterator entries();
Iterator iterator();
//
// Navigation
//
CharSequence current();
boolean previous();
boolean next();
boolean moveToFirst();
boolean moveToLast();
boolean moveTo(int index);
void moveToEnd();
}
src/main/java/jline/console/history/MemoryHistory.java 0000664 0000000 0000000 00000017273 12146220444 0023464 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.history;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.NoSuchElementException;
import static jline.internal.Preconditions.checkNotNull;
/**
* Non-persistent {@link History}.
*
* @author Marc Prud'hommeaux
* @author Jason Dillon
* @since 2.3
*/
public class MemoryHistory
implements History
{
public static final int DEFAULT_MAX_SIZE = 500;
private final LinkedList items = new LinkedList();
private int maxSize = DEFAULT_MAX_SIZE;
private boolean ignoreDuplicates = true;
private boolean autoTrim = false;
// NOTE: These are all ideas from looking at the Bash man page:
// TODO: Add ignore space? (lines starting with a space are ignored)
// TODO: Add ignore patterns?
// TODO: Add history timestamp?
// TODO: Add erase dups?
private int offset = 0;
private int index = 0;
public void setMaxSize(final int maxSize) {
this.maxSize = maxSize;
maybeResize();
}
public int getMaxSize() {
return maxSize;
}
public boolean isIgnoreDuplicates() {
return ignoreDuplicates;
}
public void setIgnoreDuplicates(final boolean flag) {
this.ignoreDuplicates = flag;
}
public boolean isAutoTrim() {
return autoTrim;
}
public void setAutoTrim(final boolean flag) {
this.autoTrim = flag;
}
public int size() {
return items.size();
}
public boolean isEmpty() {
return items.isEmpty();
}
public int index() {
return offset + index;
}
public void clear() {
items.clear();
offset = 0;
index = 0;
}
public CharSequence get(final int index) {
return items.get(index - offset);
}
public void set(int index, CharSequence item) {
items.set(index - offset, item);
}
public void add(CharSequence item) {
checkNotNull(item);
if (isAutoTrim()) {
item = String.valueOf(item).trim();
}
if (isIgnoreDuplicates()) {
if (!items.isEmpty() && item.equals(items.getLast())) {
return;
}
}
internalAdd(item);
}
public CharSequence remove(int i) {
return items.remove(i);
}
public CharSequence removeFirst() {
return items.removeFirst();
}
public CharSequence removeLast() {
return items.removeLast();
}
protected void internalAdd(CharSequence item) {
items.add(item);
maybeResize();
}
public void replace(final CharSequence item) {
items.removeLast();
add(item);
}
private void maybeResize() {
while (size() > getMaxSize()) {
items.removeFirst();
offset++;
}
index = size();
}
public ListIterator entries(final int index) {
return new EntriesIterator(index - offset);
}
public ListIterator entries() {
return entries(offset);
}
public Iterator iterator() {
return entries();
}
private static class EntryImpl
implements Entry
{
private final int index;
private final CharSequence value;
public EntryImpl(int index, CharSequence value) {
this.index = index;
this.value = value;
}
public int index() {
return index;
}
public CharSequence value() {
return value;
}
@Override
public String toString() {
return String.format("%d: %s", index, value);
}
}
private class EntriesIterator
implements ListIterator
{
private final ListIterator source;
private EntriesIterator(final int index) {
source = items.listIterator(index);
}
public Entry next() {
if (!source.hasNext()) {
throw new NoSuchElementException();
}
return new EntryImpl(offset + source.nextIndex(), source.next());
}
public Entry previous() {
if (!source.hasPrevious()) {
throw new NoSuchElementException();
}
return new EntryImpl(offset + source.previousIndex(), source.previous());
}
public int nextIndex() {
return offset + source.nextIndex();
}
public int previousIndex() {
return offset + source.previousIndex();
}
public boolean hasNext() {
return source.hasNext();
}
public boolean hasPrevious() {
return source.hasPrevious();
}
public void remove() {
throw new UnsupportedOperationException();
}
public void set(final Entry entry) {
throw new UnsupportedOperationException();
}
public void add(final Entry entry) {
throw new UnsupportedOperationException();
}
}
//
// Navigation
//
/**
* This moves the history to the last entry. This entry is one position
* before the moveToEnd() position.
*
* @return Returns false if there were no history entries or the history
* index was already at the last entry.
*/
public boolean moveToLast() {
int lastEntry = size() - 1;
if (lastEntry >= 0 && lastEntry != index) {
index = size() - 1;
return true;
}
return false;
}
/**
* Move to the specified index in the history
* @param index
* @return
*/
public boolean moveTo(int index) {
index -= offset;
if (index >= 0 && index < size() ) {
this.index = index;
return true;
}
return false;
}
/**
* Moves the history index to the first entry.
*
* @return Return false if there are no entries in the history or if the
* history is already at the beginning.
*/
public boolean moveToFirst() {
if (size() > 0 && index != 0) {
index = 0;
return true;
}
return false;
}
/**
* Move to the end of the history buffer. This will be a blank entry, after
* all of the other entries.
*/
public void moveToEnd() {
index = size();
}
/**
* Return the content of the current buffer.
*/
public CharSequence current() {
if (index >= size()) {
return "";
}
return items.get(index);
}
/**
* Move the pointer to the previous element in the buffer.
*
* @return true if we successfully went to the previous element
*/
public boolean previous() {
if (index <= 0) {
return false;
}
index--;
return true;
}
/**
* Move the pointer to the next element in the buffer.
*
* @return true if we successfully went to the next element
*/
public boolean next() {
if (index >= size()) {
return false;
}
index++;
return true;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Entry e : this) {
sb.append(e.toString() + "\n");
}
return sb.toString();
}
}
src/main/java/jline/console/history/PersistentHistory.java 0000664 0000000 0000000 00000001452 12146220444 0024344 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.history;
import java.io.IOException;
/**
* Persistent {@link History}.
*
* @author Jason Dillon
* @since 2.3
*/
public interface PersistentHistory
extends History
{
/**
* Flush all items to persistent storage.
*
* @throws IOException Flush failed
*/
void flush() throws IOException;
/**
* Purge persistent storage and {@link #clear}.
*
* @throws IOException Purge failed
*/
void purge() throws IOException;
} src/main/java/jline/console/history/package-info.java 0000664 0000000 0000000 00000000541 12146220444 0023144 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
/**
* Console history support.
*
* @since 2.0
*/
package jline.console.history; src/main/java/jline/console/internal/ 0000775 0000000 0000000 00000000000 12146220444 0020070 5 ustar 00root root 0000000 0000000 src/main/java/jline/console/internal/ConsoleReaderInputStream.java 0000664 0000000 0000000 00000006302 12146220444 0025655 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.internal;
import jline.console.ConsoleReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.util.Enumeration;
// FIXME: Clean up API and move to jline.console.runner package
/**
* An {@link InputStream} implementation that wraps a {@link ConsoleReader}.
* It is useful for setting up the {@link System#in} for a generic console.
*
* @author Marc Prud'hommeaux
* @since 2.7
*/
class ConsoleReaderInputStream
extends SequenceInputStream
{
private static InputStream systemIn = System.in;
public static void setIn() throws IOException {
setIn(new ConsoleReader());
}
public static void setIn(final ConsoleReader reader) {
System.setIn(new ConsoleReaderInputStream(reader));
}
/**
* Restore the original {@link System#in} input stream.
*/
public static void restoreIn() {
System.setIn(systemIn);
}
public ConsoleReaderInputStream(final ConsoleReader reader) {
super(new ConsoleEnumeration(reader));
}
private static class ConsoleEnumeration
implements Enumeration
{
private final ConsoleReader reader;
private ConsoleLineInputStream next = null;
private ConsoleLineInputStream prev = null;
public ConsoleEnumeration(final ConsoleReader reader) {
this.reader = reader;
}
public Object nextElement() {
if (next != null) {
InputStream n = next;
prev = next;
next = null;
return n;
}
return new ConsoleLineInputStream(reader);
}
public boolean hasMoreElements() {
// the last line was null
if ((prev != null) && (prev.wasNull == true)) {
return false;
}
if (next == null) {
next = (ConsoleLineInputStream) nextElement();
}
return next != null;
}
}
private static class ConsoleLineInputStream
extends InputStream
{
private final ConsoleReader reader;
private String line = null;
private int index = 0;
private boolean eol = false;
protected boolean wasNull = false;
public ConsoleLineInputStream(final ConsoleReader reader) {
this.reader = reader;
}
public int read() throws IOException {
if (eol) {
return -1;
}
if (line == null) {
line = reader.readLine();
}
if (line == null) {
wasNull = true;
return -1;
}
if (index >= line.length()) {
eol = true;
return '\n'; // lines are ended with a newline
}
return line.charAt(index++);
}
}
} src/main/java/jline/console/internal/ConsoleRunner.java 0000664 0000000 0000000 00000006257 12146220444 0023541 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.console.internal;
import jline.console.ConsoleReader;
import jline.console.completer.ArgumentCompleter;
import jline.console.completer.Completer;
import jline.console.history.FileHistory;
import jline.internal.Configuration;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.StringTokenizer;
// FIXME: Clean up API and move to jline.console.runner package
/**
* A pass-through application that sets the system input stream to a
* {@link ConsoleReader} and invokes the specified main method.
*
* @author Marc Prud'hommeaux
* @since 2.7
*/
public class ConsoleRunner
{
public static final String property = "jline.history";
// FIXME: This is really ugly... re-write this
public static void main(final String[] args) throws Exception {
List argList = Arrays.asList(args);
if (argList.size() == 0) {
usage();
return;
}
String historyFileName = System.getProperty(ConsoleRunner.property, null);
String mainClass = argList.remove(0);
ConsoleReader reader = new ConsoleReader();
if (historyFileName != null) {
reader.setHistory(new FileHistory(new File(Configuration.getUserHome(),
String.format(".jline-%s.%s.history", mainClass, historyFileName))));
}
else {
reader.setHistory(new FileHistory(new File(Configuration.getUserHome(),
String.format(".jline-%s.history", mainClass))));
}
String completors = System.getProperty(ConsoleRunner.class.getName() + ".completers", "");
List completorList = new ArrayList();
for (StringTokenizer tok = new StringTokenizer(completors, ","); tok.hasMoreTokens();) {
Object obj = Class.forName(tok.nextToken()).newInstance();
completorList.add((Completer) obj);
}
if (completorList.size() > 0) {
reader.addCompleter(new ArgumentCompleter(completorList));
}
ConsoleReaderInputStream.setIn(reader);
try {
Class type = Class.forName(mainClass);
Method method = type.getMethod("main", new Class[]{String[].class});
method.invoke(null);
}
finally {
// just in case this main method is called from another program
ConsoleReaderInputStream.restoreIn();
}
}
private static void usage() {
System.out.println("Usage: \n java " + "[-Djline.history='name'] "
+ ConsoleRunner.class.getName()
+ " [args]"
+ "\n\nThe -Djline.history option will avoid history"
+ "\nmangling when running ConsoleRunner on the same application."
+ "\n\nargs will be passed directly to the target class name.");
}
} src/main/java/jline/console/package-info.java 0000664 0000000 0000000 00000000521 12146220444 0021441 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
/**
* Console support.
*
* @since 2.0
*/
package jline.console; src/main/java/jline/internal/ 0000775 0000000 0000000 00000000000 12146220444 0016426 5 ustar 00root root 0000000 0000000 src/main/java/jline/internal/Configuration.java 0000664 0000000 0000000 00000013430 12146220444 0022101 0 ustar 00root root 0000000 0000000 /*
* Copyright (c) 2002-2012, the original author or authors.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package jline.internal;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Map;
import java.util.Properties;
import static jline.internal.Preconditions.checkNotNull;
/**
* Provides access to configuration values.
*
* @author Jason Dillon
* @author Guillaume Nodet
* @since 2.4
*/
public class Configuration
{
/**
* System property which can point to a file or URL containing configuration properties to load.
*
* @since 2.7
*/
public static final String JLINE_CONFIGURATION = "jline.configuration";
/**
* Default configuration file name loaded from user's home directory.
*/
public static final String JLINE_RC = ".jline.rc";
private static volatile Properties properties;
private static Properties initProperties() {
URL url = determineUrl();
Properties props = new Properties();
try {
loadProperties(url, props);
}
catch (IOException e) {
// debug here instead of warn, as this can happen normally if default jline.rc file is missing
Log.debug("Unable to read configuration from: ", url, e);
}
return props;
}
private static void loadProperties(final URL url, final Properties props) throws IOException {
Log.debug("Loading properties from: ", url);
InputStream input = url.openStream();
try {
props.load(new BufferedInputStream(input));
}
finally {
try {
input.close();
}
catch (IOException e) {
// ignore
}
}
if (Log.DEBUG) {
Log.debug("Loaded properties:");
for (Map.Entry