blob: 18339d48c4654d6ec9a1cd256a266b44ac2656be [file] [log] [blame]
/*
* 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));
names.put("ABORT", new Short(ABORT));
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();
private StringBuffer searchTerm = null;
private String previousSearchTerm = "";
private int searchIndex = -1;
/**
* Adding a triggered Action allows to give another course 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 getTerminal().getTerminalWidth();
}
/**
* Query the terminal to find the current width;
*
* @see Terminal#getTerminalHeight
*
* @return the height of the current terminal.
*/
public int getTermheight() {
return getTerminal().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 getStrippedAnsiLength(prompt) + buf.cursor;
}
/**
* Strips ANSI escape sequences starting with CSI and ending with char in range 64-126
* @param ansiString String possibly containing ANSI codes, may be null
* @return length after stripping ANSI codes
*/
int getStrippedAnsiLength(String ansiString) {
if (ansiString == null) return 0;
boolean inAnsi = false;
int strippedLength = 0;
char[] chars = ansiString.toCharArray();
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
if (!inAnsi && c == 27 && i < chars.length - 1 && chars[i+1] == '[') {
i++; // skip '['
inAnsi = true;
} else if (inAnsi) {
if (64 <= c && c <= 126) {
inAnsi = false;
}
} else {
strippedLength++;
}
}
return strippedLength;
}
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);
}
final int NORMAL = 1;
final int SEARCH = 2;
int state = NORMAL;
boolean success = true;
while (true) {
// Read next key and look up the command binding.
int[] next = readBinding();
if (next == null) {
return null;
}
int c = next[0];
int code = next[1];
if (c == -1) {
return null;
}
// 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 == SEARCH) {
switch (code) {
// This doesn't work right now, it seems CTRL-G is not passed
// down correctly. :(
case ABORT:
state = NORMAL;
break;
case SEARCH_PREV:
if (searchTerm.length() == 0) {
searchTerm.append(previousSearchTerm);
}
if (searchIndex == -1) {
searchIndex = history.searchBackwards(searchTerm.toString());
} else {
searchIndex = history.searchBackwards(searchTerm.toString(), searchIndex);
}
break;
case DELETE_PREV_CHAR:
if (searchTerm.length() > 0) {
searchTerm.deleteCharAt(searchTerm.length() - 1);
searchIndex = history.searchBackwards(searchTerm.toString());
}
break;
case UNKNOWN:
searchTerm.appendCodePoint(c);
searchIndex = history.searchBackwards(searchTerm.toString());
break;
default:
// Set buffer and cursor position to the found string.
if (searchIndex != -1) {
history.setCurrentIndex(searchIndex);
setBuffer(history.current());
buf.cursor = history.current().indexOf(searchTerm.toString());
}
state = NORMAL;
break;
}
// if we're still in search mode, print the search status
if (state == SEARCH) {
if (searchTerm.length() == 0) {
printSearchStatus("", "");
} else {
if (searchIndex == -1) {
beep();
} else {
printSearchStatus(searchTerm.toString(), history.getHistory(searchIndex));
}
}
}
// otherwise, restore the line
else {
restoreLine();
}
}
if (state == NORMAL) {
switch (code) {
case EXIT: // ctrl-d
if (buf.buffer.length() == 0) {
return null;
}
else {
success = deleteCurrentCharacter();
}
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 ABORT:
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 SEARCH_PREV: // CTRL-R
if (searchTerm != null) {
previousSearchTerm = searchTerm.toString();
}
searchTerm = new StringBuffer(buf.buffer);
state = SEARCH;
if (searchTerm.length() > 0) {
searchIndex = history.searchBackwards(searchTerm.toString());
if (searchIndex == -1) {
beep();
}
printSearchStatus(searchTerm.toString(),
searchIndex > -1 ? history.getHistory(searchIndex) : "");
} else {
searchIndex = -1;
printSearchStatus("", "");
}
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.
*/
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
printANSISequence("2J");
// then send the ANSI code to go to position 1,1
printANSISequence("1;1H");
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.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
}
/**
* 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 - 1); // 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);
if (buf.cursor == buf.length() && clear == 0) {
return;
}
char[] chars = buf.buffer.substring(buf.cursor).toCharArray();
if (mask != null) {
Arrays.fill(chars, mask.charValue());
}
printCharacters(chars);
clearAhead(clear);
if (terminal.isANSISupported()) {
if (chars.length > 0) {
// don't ask, it seems to work
back(Math.max(chars.length - 1, 1));
}
} else {
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;
}
if (terminal.isANSISupported()) {
printANSISequence("J");
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 {
if (num == 0) return;
if (terminal.isANSISupported()) {
int width = getTermwidth();
int cursor = getCursorPosition();
// debug("back: " + cursor + " + " + num + " on " + width);
int currRow = (cursor + num) / width;
int newRow = cursor / width;
int newCol = cursor % width + 1;
// debug(" old row: " + currRow + " new row: " + newRow);
if (newRow < currRow) {
printANSISequence((currRow - newRow) + "A");
}
printANSISequence(newCol + "G");
flushConsole();
return;
}
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;
int termwidth = getTermwidth();
int lines = getCursorPosition() / termwidth;
count = moveCursor(-1 * num) * -1;
// debug ("Deleting from " + buf.cursor + " for " + count);
buf.buffer.delete(buf.cursor, buf.cursor + count);
if (getCursorPosition() / termwidth != lines) {
if (terminal.isANSISupported()) {
// debug("doing backspace redraw: " + getCursorPosition() + " on " + termwidth + ": " + lines);
printANSISequence("J");
flushConsole();
}
}
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 {
return moveCursor(buf.length() - buf.cursor) > 0;
}
/**
* Delete the character at the current position and redraw the remainder of
* the buffer.
*/
private final boolean deleteCurrentCharacter() throws IOException {
if (buf.length() == 0 || buf.cursor == buf.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 num
* if less than 0, move abs(<i>num</i>) to the left,
* otherwise move <i>num</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.
*
* @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;
if (terminal.isANSISupported()) {
if (where < 0) {
back(Math.abs(where));
} else {
int width = getTermwidth();
int cursor = getCursorPosition();
int oldLine = (cursor - where) / width;
int newLine = cursor / width;
if (newLine > oldLine) {
printANSISequence((newLine - oldLine) + "B");
}
printANSISequence(1 +(cursor % width) + "G");
}
flushConsole();
return;
}
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);
}
private void printANSISequence(String sequence) throws IOException {
printCharacter(27);
printCharacter('[');
printString(sequence);
flushConsole();
}
/*
private int currentCol, currentRow;
private void getCurrentPosition() {
// check for ByteArrayInputStream to disable for unit tests
if (terminal.isANSISupported() && !(in instanceof ByteArrayInputStream)) {
try {
printANSISequence("[6n");
flushConsole();
StringBuffer b = new StringBuffer(8);
// position is sent as <ESC>[{ROW};{COLUMN}R
int r;
while((r = in.read()) > -1 && r != 'R') {
if (r != 27 && r != '[') {
b.append((char) r);
}
}
String[] pos = b.toString().split(";");
currentRow = Integer.parseInt(pos[0]);
currentCol = Integer.parseInt(pos[1]);
} catch (Exception x) {
// no luck
currentRow = currentCol = -1;
}
}
}
*/
/**
* 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;
}
public void printSearchStatus(String searchTerm, String match) throws IOException {
int i = match.indexOf(searchTerm);
printString("\r(reverse-i-search) `" + searchTerm + "': " + match + "\u001b[K");
// FIXME: our ANSI using back() does not work here
printCharacters(BACKSPACE, match.length() - i);
flushConsole();
}
public void restoreLine() throws IOException {
printString("\u001b[2K"); // ansi/vt100 for clear whole line
redrawLine();
flushConsole();
}
}