/*
* Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*/
package jline;
import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.
ActionListener;
import java.io.*;
import java.util.*;
import java.util.
List;
/**
* 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 Terminal#initializeTerminal} for convenience
* methods for issuing platform-specific setup commands.
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
public class
ConsoleReader implements
ConsoleOperations {
final static int
TAB_WIDTH = 4;
String prompt;
private boolean
useHistory = true;
private boolean
usePagination = false;
public static final
String CR =
System.
getProperty("line.separator");
private static
ResourceBundle loc =
ResourceBundle
.
getBundle(
CandidateListCompletionHandler.class.
getName());
/**
* Map that contains the operation name to keymay operation mapping.
*/
public static
SortedMap KEYMAP_NAMES;
static {
Map names = new
TreeMap();
names.
put("MOVE_TO_BEG", new
Short(
MOVE_TO_BEG));
names.
put("MOVE_TO_END", new
Short(
MOVE_TO_END));
names.
put("PREV_CHAR", new
Short(
PREV_CHAR));
names.
put("NEWLINE", new
Short(
NEWLINE));
names.
put("KILL_LINE", new
Short(
KILL_LINE));
names.
put("PASTE", new
Short(
PASTE));
names.
put("CLEAR_SCREEN", new
Short(
CLEAR_SCREEN));
names.
put("NEXT_HISTORY", new
Short(
NEXT_HISTORY));
names.
put("PREV_HISTORY", new
Short(
PREV_HISTORY));
names.
put("START_OF_HISTORY", new
Short(
START_OF_HISTORY));
names.
put("END_OF_HISTORY", new
Short(
END_OF_HISTORY));
names.
put("REDISPLAY", new
Short(
REDISPLAY));
names.
put("KILL_LINE_PREV", new
Short(
KILL_LINE_PREV));
names.
put("DELETE_PREV_WORD", new
Short(
DELETE_PREV_WORD));
names.
put("NEXT_CHAR", new
Short(
NEXT_CHAR));
names.
put("REPEAT_PREV_CHAR", new
Short(
REPEAT_PREV_CHAR));
names.
put("SEARCH_PREV", new
Short(
SEARCH_PREV));
names.
put("REPEAT_NEXT_CHAR", new
Short(
REPEAT_NEXT_CHAR));
names.
put("SEARCH_NEXT", new
Short(
SEARCH_NEXT));
names.
put("PREV_SPACE_WORD", new
Short(
PREV_SPACE_WORD));
names.
put("TO_END_WORD", new
Short(
TO_END_WORD));
names.
put("REPEAT_SEARCH_PREV", new
Short(
REPEAT_SEARCH_PREV));
names.
put("PASTE_PREV", new
Short(
PASTE_PREV));
names.
put("REPLACE_MODE", new
Short(
REPLACE_MODE));
names.
put("SUBSTITUTE_LINE", new
Short(
SUBSTITUTE_LINE));
names.
put("TO_PREV_CHAR", new
Short(
TO_PREV_CHAR));
names.
put("NEXT_SPACE_WORD", new
Short(
NEXT_SPACE_WORD));
names.
put("DELETE_PREV_CHAR", new
Short(
DELETE_PREV_CHAR));
names.
put("ADD", new
Short(
ADD));
names.
put("PREV_WORD", new
Short(
PREV_WORD));
names.
put("CHANGE_META", new
Short(
CHANGE_META));
names.
put("DELETE_META", new
Short(
DELETE_META));
names.
put("END_WORD", new
Short(
END_WORD));
names.
put("NEXT_CHAR", new
Short(
NEXT_CHAR));
names.
put("INSERT", new
Short(
INSERT));
names.
put("REPEAT_SEARCH_NEXT", new
Short(
REPEAT_SEARCH_NEXT));
names.
put("PASTE_NEXT", new
Short(
PASTE_NEXT));
names.
put("REPLACE_CHAR", new
Short(
REPLACE_CHAR));
names.
put("SUBSTITUTE_CHAR", new
Short(
SUBSTITUTE_CHAR));
names.
put("TO_NEXT_CHAR", new
Short(
TO_NEXT_CHAR));
names.
put("UNDO", new
Short(
UNDO));
names.
put("NEXT_WORD", new
Short(
NEXT_WORD));
names.
put("DELETE_NEXT_CHAR", new
Short(
DELETE_NEXT_CHAR));
names.
put("CHANGE_CASE", new
Short(
CHANGE_CASE));
names.
put("COMPLETE", new
Short(
COMPLETE));
names.
put("EXIT", new
Short(
EXIT));
names.
put("CLEAR_LINE", new
Short(
CLEAR_LINE));
KEYMAP_NAMES = new
TreeMap(
Collections.
unmodifiableMap(
names));
}
/**
* The map for logical operations.
*/
private final short[]
keybindings;
/**
* If true, issue an audible keyboard bell when appropriate.
*/
private boolean
bellEnabled = true;
/**
* The current character mask.
*/
private
Character mask = null;
/**
* The null mask.
*/
private static final
Character NULL_MASK = new
Character((char) 0);
/**
* The number of tab-completion candidates above which a warning will be
* prompted before showing all the candidates.
*/
private int
autoprintThreshhold =
Integer.
getInteger(
"jline.completion.threshold", 100).
intValue(); // same default as
// bash
/**
* The Terminal to use.
*/
private final
Terminal terminal;
private
CompletionHandler completionHandler = new
CandidateListCompletionHandler();
InputStream in;
final
Writer out;
final
CursorBuffer buf = new
CursorBuffer();
static
PrintWriter debugger;
History history = new
History();
final
List completors = new
LinkedList();
private
Character echoCharacter = null;
private
Map triggeredActions = new
HashMap();
/**
* Adding a triggered Action allows to give another curse of action
* if a character passed the preprocessing.
*
* Say you want to close the application if the user enter q.
* addTriggerAction('q', new ActionListener(){ System.exit(0); });
* would do the trick.
*
* @param c
* @param listener
*/
public void
addTriggeredAction(char
c,
ActionListener listener){
triggeredActions.
put(new
Character(
c),
listener);
}
/**
* Create a new reader using {@link FileDescriptor#in} for input and
* {@link System#out} for output. {@link FileDescriptor#in} is used because
* it has a better chance of being unbuffered.
*/
public
ConsoleReader() throws
IOException {
this(new
FileInputStream(
FileDescriptor.
in),
new
PrintWriter(
new
OutputStreamWriter(
System.
out,
System.
getProperty("jline.WindowsTerminal.output.encoding",
System.
getProperty("file.encoding")))));
}
/**
* Create a new reader using the specified {@link InputStream} for input and
* the specific writer for output, using the default keybindings resource.
*/
public
ConsoleReader(final
InputStream in, final
Writer out)
throws
IOException {
this(
in,
out, null);
}
public
ConsoleReader(final
InputStream in, final
Writer out,
final
InputStream bindings) throws
IOException {
this(
in,
out,
bindings,
Terminal.
getTerminal());
}
/**
* Create a new reader.
*
* @param in
* the input
* @param out
* the output
* @param bindings
* the key bindings to use
* @param term
* the terminal to use
*/
public
ConsoleReader(
InputStream in,
Writer out,
InputStream bindings,
Terminal term) throws
IOException {
this.
terminal =
term;
setInput(
in);
this.
out =
out;
if (
bindings == null) {
try {
String bindingFile =
System.
getProperty("jline.keybindings",
new
File(
System.
getProperty("user.home",
".jlinebindings.properties")).
getAbsolutePath());
if (new
File(
bindingFile).
isFile()) {
bindings = new
FileInputStream(new
File(
bindingFile));
}
} catch (
Exception e) {
// swallow exceptions with option debugging
if (
debugger != null) {
e.
printStackTrace(
debugger);
}
}
}
if (
bindings == null) {
bindings =
terminal.
getDefaultBindings();
}
this.
keybindings = new short[
Character.
MAX_VALUE * 2];
Arrays.
fill(this.
keybindings,
UNKNOWN);
/**
* Loads the key bindings. Bindings file is in the format:
*
* keycode: operation name
*/
if (
bindings != null) {
Properties p = new
Properties();
p.
load(
bindings);
bindings.
close();
for (
Iterator i =
p.
keySet().
iterator();
i.
hasNext();) {
String val = (
String)
i.
next();
try {
Short code = new
Short(
val);
String op = (
String)
p.
getProperty(
val);
Short opval = (
Short)
KEYMAP_NAMES.
get(
op);
if (
opval != null) {
keybindings[
code.
shortValue()] =
opval.
shortValue();
}
} catch (
NumberFormatException nfe) {
consumeException(
nfe);
}
}
// hardwired arrow key bindings
// keybindings[VK_UP] = PREV_HISTORY;
// keybindings[VK_DOWN] = NEXT_HISTORY;
// keybindings[VK_LEFT] = PREV_CHAR;
// keybindings[VK_RIGHT] = NEXT_CHAR;
}
}
public
Terminal getTerminal() {
return this.
terminal;
}
/**
* Set the stream for debugging. Development use only.
*/
public void
setDebug(final
PrintWriter debugger) {
ConsoleReader.
debugger =
debugger;
}
/**
* Set the stream to be used for console input.
*/
public void
setInput(final
InputStream in) {
this.
in =
in;
}
/**
* Returns the stream used for console input.
*/
public
InputStream getInput() {
return this.
in;
}
/**
* 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);
}
/**
* @param bellEnabled
* if true, enable audible keyboard bells if an alert is
* required.
*/
public void
setBellEnabled(final boolean
bellEnabled) {
this.
bellEnabled =
bellEnabled;
}
/**
* @return true is audible keyboard bell is enabled.
*/
public boolean
getBellEnabled() {
return this.
bellEnabled;
}
/**
* Query the terminal to find the current width;
*
* @see Terminal#getTerminalWidth
* @return the width of the current terminal.
*/
public int
getTermwidth() {
return
Terminal.
setupTerminal().
getTerminalWidth();
}
/**
* Query the terminal to find the current width;
*
* @see Terminal#getTerminalHeight
*
* @return the height of the current terminal.
*/
public int
getTermheight() {
return
Terminal.
setupTerminal().
getTerminalHeight();
}
/**
* @param autoprintThreshhold
* the number of candidates to print without issuing a warning.
*/
public void
setAutoprintThreshhold(final int
autoprintThreshhold) {
this.
autoprintThreshhold =
autoprintThreshhold;
}
/**
* @return the number of candidates to print without issing a warning.
*/
public int
getAutoprintThreshhold() {
return this.
autoprintThreshhold;
}
int
getKeyForAction(short
logicalAction) {
for (int
i = 0;
i <
keybindings.length;
i++) {
if (
keybindings[
i] ==
logicalAction) {
return
i;
}
}
return -1;
}
/**
* Clear the echoed characters for the specified character code.
*/
int
clearEcho(int
c) throws
IOException {
// if the terminal is not echoing, then just return...
if (!
terminal.
getEcho()) {
return 0;
}
// otherwise, clear
int
num =
countEchoCharacters((char)
c);
back(
num);
drawBuffer(
num);
return
num;
}
int
countEchoCharacters(char
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.
*/
StringBuffer getPrintableCharacters(char
ch) {
StringBuffer sbuff = new
StringBuffer();
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;
}
int
getCursorPosition() {
// FIXME: does not handle anything but a line with a prompt
// absolute position
return ((
prompt == null) ? 0 :
prompt.
length()) +
buf.
cursor;
}
public
String readLine(final
String prompt) throws
IOException {
return
readLine(
prompt, null);
}
/**
* The default prompt that will be issued.
*/
public void
setDefaultPrompt(
String prompt) {
this.
prompt =
prompt;
}
/**
* The default prompt that will be issued.
*/
public
String getDefaultPrompt() {
return
prompt;
}
/**
* Read a line from the <i>in</i> {@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., <i>CTRL-D</i> was pressed).
*/
public
String readLine(final
String prompt, final
Character mask)
throws
IOException {
this.
mask =
mask;
if (
prompt != null)
this.
prompt =
prompt;
try {
terminal.
beforeReadLine(this, this.
prompt,
mask);
if ((this.
prompt != null) && (this.
prompt.
length() > 0)) {
out.
write(this.
prompt);
out.
flush();
}
// if the terminal is unsupported, just use plain-java reading
if (!
terminal.
isSupported()) {
return
readLine(
in);
}
while (true) {
int[]
next =
readBinding();
if (
next == null) {
return null;
}
int
c =
next[0];
int
code =
next[1];
if (
c == -1) {
return null;
}
boolean
success = true;
switch (
code) {
case
EXIT: // ctrl-d
if (
buf.
buffer.
length() == 0) {
return null;
}
break;
case
COMPLETE: // tab
success =
complete();
break;
case
MOVE_TO_BEG:
success =
setCursorPosition(0);
break;
case
KILL_LINE: // CTRL-K
success =
killLine();
break;
case
CLEAR_SCREEN: // CTRL-L
success =
clearScreen();
break;
case
KILL_LINE_PREV: // CTRL-U
success =
resetLine();
break;
case
NEWLINE: // enter
moveToEnd();
printNewline(); // output newline
return
finishBuffer();
case
DELETE_PREV_CHAR: // backspace
success =
backspace();
break;
case
DELETE_NEXT_CHAR: // delete
success =
deleteCurrentCharacter();
break;
case
MOVE_TO_END:
success =
moveToEnd();
break;
case
PREV_CHAR:
success =
moveCursor(-1) != 0;
break;
case
NEXT_CHAR:
success =
moveCursor(1) != 0;
break;
case
NEXT_HISTORY:
success =
moveHistory(true);
break;
case
PREV_HISTORY:
success =
moveHistory(false);
break;
case
REDISPLAY:
break;
case
PASTE:
success =
paste();
break;
case
DELETE_PREV_WORD:
success =
deletePreviousWord();
break;
case
PREV_WORD:
success =
previousWord();
break;
case
NEXT_WORD:
success =
nextWord();
break;
case
START_OF_HISTORY:
success =
history.
moveToFirstEntry();
if (
success)
setBuffer(
history.
current());
break;
case
END_OF_HISTORY:
success =
history.
moveToLastEntry();
if (
success)
setBuffer(
history.
current());
break;
case
CLEAR_LINE:
moveInternal(-(
buf.
buffer.
length()));
killLine();
break;
case
INSERT:
buf.
setOvertyping(!
buf.
isOvertyping());
break;
case
UNKNOWN:
default:
if (
c != 0) { // ignore null chars
ActionListener action = (
ActionListener)
triggeredActions.
get(new
Character((char)
c));
if (
action != null)
action.
actionPerformed(null);
else
putChar(
c, true);
} else
success = false;
}
if (!(
success)) {
beep();
}
flushConsole();
}
} finally {
terminal.
afterReadLine(this, this.
prompt,
mask);
}
}
private
String readLine(
InputStream in) throws
IOException {
StringBuffer buf = new
StringBuffer();
while (true) {
int
i =
in.
read();
if ((
i == -1) || (
i == '\n') || (
i == '\r')) {
return
buf.
toString();
}
buf.
append((char)
i);
}
// return new BufferedReader (new InputStreamReader (in)).readLine ();
}
/**
* Reads the console input and returns an array of the form [raw, key
* binding].
*/
private int[]
readBinding() throws
IOException {
int
c =
readVirtualKey();
if (
c == -1) {
return null;
}
// extract the appropriate key binding
short
code =
keybindings[
c];
if (
debugger != null) {
debug(" translated: " + (int)
c + ": " +
code);
}
return new int[] {
c,
code };
}
/**
* Move up or down the history tree.
*
* @param direction
* less than 0 to move up the tree, down otherwise
*/
private final 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;
}
/**
* 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) {
}
}
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 = null;
for (
BufferedReader read = new
BufferedReader((
Reader)
content); (
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 ufe) {
if (
debugger != null)
debug(
ufe + "");
return false;
}
}
/**
* 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);
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
printString(((char) 27) + "[2J");
flushConsole();
// then send the ANSI code to go to position 1,1
printString(((char) 27) + "[1;1H");
flushConsole();
redrawLine();
return true;
}
/**
* Use the completors to modify the buffer with the appropriate completions.
*
* @return true if successful
*/
private final boolean
complete() throws
IOException {
// debug ("tab for (" + buf + ")");
if (
completors.
size() == 0) {
return false;
}
List candidates = new
LinkedList();
String bufstr =
buf.
buffer.
toString();
int
cursor =
buf.
cursor;
int
position = -1;
for (
Iterator i =
completors.
iterator();
i.
hasNext();) {
Completor comp = (
Completor)
i.
next();
if ((
position =
comp.
complete(
bufstr,
cursor,
candidates)) != -1) {
break;
}
}
// no candidates? Fail.
if (
candidates.
size() == 0) {
return false;
}
return
completionHandler.
complete(this,
candidates,
position);
}
public
CursorBuffer getCursorBuffer() {
return
buf;
}
/**
* Output the specified {@link Collection} in proper columns.
*
* @param stuff
* the stuff to print
*/
public void
printColumns(final
Collection stuff) throws
IOException {
if ((
stuff == null) || (
stuff.
size() == 0)) {
return;
}
int
width =
getTermwidth();
int
maxwidth = 0;
for (
Iterator i =
stuff.
iterator();
i.
hasNext();
maxwidth =
Math.
max(
maxwidth,
i.
next().
toString().
length())) {
;
}
StringBuffer line = new
StringBuffer();
int
showLines;
if (
usePagination)
showLines =
getTermheight() - 1; // page limit
else
showLines =
Integer.
MAX_VALUE;
for (
Iterator i =
stuff.
iterator();
i.
hasNext();) {
String cur = (
String)
i.
next();
if ((
line.
length() +
maxwidth) >
width) {
printString(
line.
toString().
trim());
printNewline();
line.
setLength(0);
if (--
showLines == 0) { // Overflow
printString(
loc.
getString("display-more"));
flushConsole();
int
c =
readVirtualKey();
if (
c == '\r' ||
c == '\n')
showLines = 1; // one step forward
else if (
c != 'q')
showLines =
getTermheight() - 1; // page forward
back(
loc.
getString("display-more").
length());
if (
c == 'q')
break; // cancel
}
}
pad(
cur,
maxwidth + 3,
line);
}
if (
line.
length() > 0) {
printString(
line.
toString().
trim());
printNewline();
line.
setLength(0);
}
}
/**
* Append <i>toPad</i> to the specified <i>appendTo</i>, as well as (<i>toPad.length () -
* len</i>) spaces.
*
* @param toPad
* the {@link String} to pad
* @param len
* the target length
* @param appendTo
* the {@link StringBuffer} to which to append the padded
* {@link String}.
*/
private final void
pad(final
String toPad, final int
len,
final
StringBuffer appendTo) {
appendTo.
append(
toPad);
for (int
i = 0;
i < (
len -
toPad.
length());
i++,
appendTo.
append(' ')) {
;
}
}
/**
* Add the specified {@link Completor} to the list of handlers for
* tab-completion.
*
* @param completor
* the {@link Completor} to add
* @return true if it was successfully added
*/
public boolean
addCompletor(final
Completor completor) {
return
completors.
add(
completor);
}
/**
* Remove the specified {@link Completor} from the list of handlers for
* tab-completion.
*
* @param completor
* the {@link Completor} to remove
* @return true if it was successfully removed
*/
public boolean
removeCompletor(final
Completor completor) {
return
completors.
remove(
completor);
}
/**
* Returns an unmodifiable list of all the completors.
*/
public
Collection getCompletors() {
return
Collections.
unmodifiableList(
completors);
}
/**
* Erase the current line.
*
* @return false if we failed (e.g., the buffer was empty)
*/
final boolean
resetLine() throws
IOException {
if (
buf.
cursor == 0) {
return false;
}
backspaceAll();
return true;
}
/**
* Move the cursor position to the specified absolute index.
*/
public final boolean
setCursorPosition(final int
position)
throws
IOException {
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 final 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.
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
}
/**
* Clear the line and redraw it.
*/
public final void
redrawLine() throws
IOException {
printCharacter(
RESET_LINE);
flushConsole();
drawLine();
}
/**
* Output put the prompt + the current buffer
*/
public final void
drawLine() throws
IOException {
if (
prompt != null) {
printString(
prompt);
}
printString(
buf.
buffer.
toString());
if (
buf.
length() !=
buf.
cursor) // not at end of line
back(
buf.
length() -
buf.
cursor); // sync
}
/**
* Output a platform-dependant newline.
*/
public final void
printNewline() throws
IOException {
printString(
CR);
flushConsole();
}
/**
* Clear the buffer and add its contents to the history.
*
* @return the former contents of the buffer.
*/
final
String finishBuffer() {
String str =
buf.
buffer.
toString();
// 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 &&
useHistory) {
history.
addToHistory(
str);
} else {
mask = null;
}
}
history.
moveToEnd();
buf.
buffer.
setLength(0);
buf.
cursor = 0;
return
str;
}
/**
* Write out the specified string to the buffer and the output stream.
*/
public final void
putString(final
String str) throws
IOException {
buf.
write(
str);
printString(
str);
drawBuffer();
}
/**
* Output the specified string to the output stream (but not the buffer).
*/
public final void
printString(final
String str) throws
IOException {
printCharacters(
str.
toCharArray());
}
/**
* Output the specified character, both to the buffer and the output stream.
*/
private final void
putChar(final int
c, final boolean
print)
throws
IOException {
buf.
write((char)
c);
if (
print) {
// no masking...
if (
mask == null) {
printCharacter(
c);
}
// null mask: don't print anything...
else if (
mask.
charValue() == 0) {
;
}
// otherwise print the mask...
else {
printCharacter(
mask.
charValue());
}
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 final void
drawBuffer(final int
clear) throws
IOException {
// debug ("drawBuffer: " + clear);
char[]
chars =
buf.
buffer.
substring(
buf.
cursor).
toCharArray();
if (
mask != null)
Arrays.
fill(
chars,
mask.
charValue());
printCharacters(
chars);
clearAhead(
clear);
back(
chars.length);
flushConsole();
}
/**
* Redraw the rest of the buffer from the cursor onwards. This is necessary
* for inserting text into the buffer.
*/
private final void
drawBuffer() throws
IOException {
drawBuffer(0);
}
/**
* Clear ahead the specified number of characters without moving the cursor.
*/
private final void
clearAhead(final int
num) throws
IOException {
if (
num == 0) {
return;
}
// debug ("clearAhead: " + num);
// print blank extra characters
printCharacters(' ',
num);
// we need to flush here so a "clever" console
// doesn't just ignore the redundancy of a space followed by
// a backspace.
flushConsole();
// reset the visual cursor
back(
num);
flushConsole();
}
/**
* Move the visual cursor backwards without modifying the buffer cursor.
*/
private final void
back(final int
num) throws
IOException {
printCharacters(
BACKSPACE,
num);
flushConsole();
}
/**
* Issue an audible keyboard bell, if {@link #getBellEnabled} return true.
*/
public final void
beep() throws
IOException {
if (!(
getBellEnabled())) {
return;
}
printCharacter(
KEYBOARD_BELL);
// need to flush so the console actually beeps
flushConsole();
}
/**
* Output the specified character to the output stream without manipulating
* the current buffer.
*/
private final void
printCharacter(final int
c) throws
IOException {
if (
c == '\t') {
char
cbuf[] = new char[
TAB_WIDTH];
Arrays.
fill(
cbuf, ' ');
out.
write(
cbuf);
return;
}
out.
write(
c);
}
/**
* Output the specified characters to the output stream without manipulating
* the current buffer.
*/
private final void
printCharacters(final char[]
c) throws
IOException {
int
len = 0;
for (int
i = 0;
i <
c.length;
i++)
if (
c[
i] == '\t')
len +=
TAB_WIDTH;
else
len++;
char
cbuf[];
if (
len ==
c.length)
cbuf =
c;
else {
cbuf = new char[
len];
int
pos = 0;
for (int
i = 0;
i <
c.length;
i++){
if (
c[
i] == '\t') {
Arrays.
fill(
cbuf,
pos,
pos +
TAB_WIDTH, ' ');
pos +=
TAB_WIDTH;
} else {
cbuf[
pos] =
c[
i];
pos++;
}
}
}
out.
write(
cbuf);
}
private final void
printCharacters(final char
c, final int
num)
throws
IOException {
if (
num == 1) {
printCharacter(
c);
} else {
char[]
chars = new char[
num];
Arrays.
fill(
chars,
c);
printCharacters(
chars);
}
}
/**
* 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 immedately.
*/
public final void
flushConsole() throws
IOException {
out.
flush();
}
private final int
backspaceAll() throws
IOException {
return
backspace(
Integer.
MAX_VALUE);
}
/**
* Issue <em>num</em> backspaces.
*
* @return the number of characters backed up
*/
private final int
backspace(final int
num) throws
IOException {
if (
buf.
cursor == 0) {
return 0;
}
int
count = 0;
count =
moveCursor(-1 *
num) * -1;
// debug ("Deleting from " + buf.cursor + " for " + count);
buf.
buffer.
delete(
buf.
cursor,
buf.
cursor +
count);
drawBuffer(
count);
return
count;
}
/**
* Issue a backspace.
*
* @return true if successful
*/
public final boolean
backspace() throws
IOException {
return
backspace(1) == 1;
}
private final boolean
moveToEnd() throws
IOException {
if (
moveCursor(1) == 0) {
return false;
}
while (
moveCursor(1) != 0) {
;
}
return true;
}
/**
* Delete the character at the current position and redraw the remainder of
* the buffer.
*/
private final boolean
deleteCurrentCharacter() throws
IOException {
boolean
success =
buf.
buffer.
length() > 0;
if (!
success) {
return false;
}
if (
buf.
cursor ==
buf.
buffer.
length()) {
return false;
}
buf.
buffer.
deleteCharAt(
buf.
cursor);
drawBuffer(1);
return true;
}
private final boolean
previousWord() throws
IOException {
while (
isDelimiter(
buf.
current()) && (
moveCursor(-1) != 0)) {
;
}
while (!
isDelimiter(
buf.
current()) && (
moveCursor(-1) != 0)) {
;
}
return true;
}
private final boolean
nextWord() throws
IOException {
while (
isDelimiter(
buf.
current()) && (
moveCursor(1) != 0)) {
;
}
while (!
isDelimiter(
buf.
current()) && (
moveCursor(1) != 0)) {
;
}
return true;
}
private final boolean
deletePreviousWord() throws
IOException {
while (
isDelimiter(
buf.
current()) &&
backspace()) {
;
}
while (!
isDelimiter(
buf.
current()) &&
backspace()) {
;
}
return true;
}
/**
* Move the cursor <i>where</i> characters.
*
* @param where
* if less than 0, move abs(<i>where</i>) to the left,
* otherwise move <i>where</i> to the right.
*
* @return the number of spaces we moved
*/
public final 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;
}
/**
* debug.
*
* @param str
* the message to issue.
*/
public static void
debug(final
String str) {
if (
debugger != null) {
debugger.
println(
str);
debugger.
flush();
}
}
/**
* Move the cursor <i>where</i> characters, withough checking the current
* buffer.
*
* @see #where
*
* @param where
* the number of characters to move to the right or left.
*/
private final void
moveInternal(final int
where) throws
IOException {
// debug ("move cursor " + where + " ("
// + buf.cursor + " => " + (buf.cursor + where) + ")");
buf.
cursor +=
where;
char
c;
if (
where < 0) {
int
len = 0;
for (int
i =
buf.
cursor;
i <
buf.
cursor -
where;
i++){
if (
buf.
getBuffer().
charAt(
i) == '\t')
len +=
TAB_WIDTH;
else
len++;
}
char
cbuf[] = new char[
len];
Arrays.
fill(
cbuf,
BACKSPACE);
out.
write(
cbuf);
return;
} else if (
buf.
cursor == 0) {
return;
} else if (
mask != null) {
c =
mask.
charValue();
} else {
printCharacters(
buf.
buffer.
substring(
buf.
cursor -
where,
buf.
cursor).
toCharArray());
return;
}
// null character mask: don't output anything
if (
NULL_MASK.
equals(
mask)) {
return;
}
printCharacters(
c,
Math.
abs(
where));
}
/**
* Read a character from the console.
*
* @return the character, or -1 if an EOF is received.
*/
public final int
readVirtualKey() throws
IOException {
int
c =
terminal.
readVirtualKey(
in);
if (
debugger != null) {
debug("keystroke: " +
c + "");
}
// clear any echo characters
clearEcho(
c);
return
c;
}
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)
readVirtualKey()) < 0)
;
return
c;
}
/**
* Issue <em>num</em> deletes.
*
* @return the number of characters backed up
*/
private final int
delete (final int
num)
throws
IOException
{
/* Commented out beacuse of DWA-2949:
if (buf.cursor == 0)
return 0;*/
buf.
buffer.
delete (
buf.
cursor,
buf.
cursor + 1);
drawBuffer (1);
return 1;
}
public final boolean
replace(int
num,
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;
}
/**
* Issue a delete.
*
* @return true if successful
*/
public final boolean
delete ()
throws
IOException
{
return
delete (1) == 1;
}
public void
setHistory(final
History history) {
this.
history =
history;
}
public
History getHistory() {
return this.
history;
}
public void
setCompletionHandler(final
CompletionHandler completionHandler) {
this.
completionHandler =
completionHandler;
}
public
CompletionHandler getCompletionHandler() {
return this.
completionHandler;
}
/**
* <p>
* Set the echo character. For example, to have "*" entered when a password
* is typed:
* </p>
*
* <pre>
* myConsoleReader.setEchoCharacter(new Character('*'));
* </pre>
*
* <p>
* Setting the character to
*
* <pre>
* null
* </pre>
*
* will restore normal character echoing. Setting the character to
*
* <pre>
* new Character(0)
* </pre>
*
* will cause nothing to be echoed.
* </p>
*
* @param echoCharacter
* the character to echo to the console in place of the typed
* character.
*/
public void
setEchoCharacter(final
Character echoCharacter) {
this.
echoCharacter =
echoCharacter;
}
/**
* Returns the echo character.
*/
public
Character getEchoCharacter() {
return this.
echoCharacter;
}
/**
* No-op for exceptions we want to silently consume.
*/
private void
consumeException(final
Throwable e) {
}
/**
* 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(char
c) {
return !
Character.
isLetterOrDigit(
c);
}
/**
* Whether or not to add new commands to the history buffer.
*/
public void
setUseHistory(boolean
useHistory) {
this.
useHistory =
useHistory;
}
/**
* Whether or not to add new commands to the history buffer.
*/
public boolean
getUseHistory() {
return
useHistory;
}
/**
* Whether to use pagination when the number of rows of candidates exceeds
* the height of the temrinal.
*/
public void
setUsePagination(boolean
usePagination) {
this.
usePagination =
usePagination;
}
/**
* Whether to use pagination when the number of rows of candidates exceeds
* the height of the temrinal.
*/
public boolean
getUsePagination() {
return this.
usePagination;
}
}