/*
* 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 org.jboss.forge.shell.console.jline.console;
import java.awt.Toolkit;
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.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import org.fusesource.jansi.AnsiOutputStream;
import org.jboss.forge.shell.Shell;
import org.jboss.forge.shell.integration.BufferManager;
import org.jboss.forge.shell.integration.KeyListener;
/**
* 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 org.jboss.forge.shell.console.jline.Terminal#init} for convenience methods for issuing
* platform-specific setup commands.
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
* @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
*/
public class ConsoleReader
{
public static final String JLINE_NOBELL = "jline.nobell";
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(org.jboss.forge.shell.console.jline.console.completer.CandidateListCompletionHandler.class
.getName() + "Bundle");
private final org.jboss.forge.shell.console.jline.Terminal terminal;
private InputStream in;
private final Shell shell;
// private final Writer out;
private final CursorBuffer buf = new CursorBuffer();
private String prompt;
private boolean bellEnabled = true;
private Character mask;
private char echoCharacter;
private StringBuffer searchTerm = null;
private String previousSearchTerm = "";
private int searchIndex = -1;
private final List<KeyListener> keyListeners = new ArrayList<KeyListener>();
public ConsoleReader(final InputStream in, final Shell shell, final InputStream bindings,
final org.jboss.forge.shell.console.jline.Terminal term) throws
IOException
{
this.in = in;
this.shell = shell;
this.terminal = term;
this.keyBindings = loadKeyBindings(bindings);
setBellEnabled(!org.jboss.forge.shell.console.jline.internal.Configuration.getBoolean(JLINE_NOBELL, false));
}
public ConsoleReader(final InputStream in, final Shell shell, final org.jboss.forge.shell.console.jline.Terminal term)
throws IOException
{
this(in, shell, null, term);
}
// FIXME: Only used for tests
void setInput(final InputStream in)
{
this.in = in;
}
public void registerKeyListener(final KeyListener keyListener)
{
keyListeners.add(keyListener);
}
public InputStream getInput()
{
return in;
}
public Shell getShell()
{
return shell;
}
public org.jboss.forge.shell.console.jline.Terminal getTerminal()
{
return terminal;
}
public CursorBuffer getCursorBuffer()
{
return buf;
}
public void setBellEnabled(final boolean enabled)
{
this.bellEnabled = enabled;
}
public boolean isBellEnabled()
{
return bellEnabled;
}
public void setPrompt(final String prompt)
{
this.prompt = prompt;
}
public String getPrompt()
{
return prompt;
}
/**
* 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
* <p/>
*
* <pre>
* null
* </pre>
* <p/>
* will restore normal character echoing. Setting the character to
* <p/>
*
* <pre>
* new Character(0)
* </pre>
* <p/>
* 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)
*/
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
String prompt = getPrompt();
return ((prompt == null) ? 0 : stripAnsi(lastLine(prompt)).length()) + 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(final 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(final 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
{
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);
}
}
/**
* 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();
str = expandEvents(str);
// 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(str);
}
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
*
* @param str
* @return
*/
final String expandEvents(final String str) throws IOException
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < str.length(); i++)
{
char c = str.charAt(i);
switch (c)
{
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 < 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)).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;
}
/* Handle case where terminal does not move cursor to the next line
* when a character is inserted at the width of the terminal. This also
* fixes backspace issue, where it assumes that the terminal is doing this.
*/
private final void newlineAtWrap() throws IOException
{
int width = getTerminal().getWidth();
if (((getCursorPosition() % width) == 0) && (getCurrentPosition() >= width))
println();
}
/**
* Write out the specified string to the buffer and the output stream.
*/
public final void putString(final CharSequence str) throws IOException
{
buf.write(str);
print(str);
drawBuffer();
newlineAtWrap();
}
/**
* Output the specified character, both to the buffer and the output stream.
*/
private void putChar(final int c, final boolean print) throws IOException
{
buf.write((char) c);
if (print)
{
if (mask == null)
{
// no masking
print(c);
}
else if (mask == NULL_MASK)
{
// Don't print anything
}
else
{
print(mask);
}
drawBuffer();
newlineAtWrap();
}
}
/**
* 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))
{
return;
}
byte[] chars = buf.buffer.substring(buf.cursor).getBytes();
if (mask != null)
{
Arrays.fill(chars, (byte) mask.charValue());
}
print(chars);
clearAhead(clear);
if (terminal.isAnsiSupported())
{
if (chars.length > 0)
{
// don't ask, it works
back(Math.max(chars.length - 1, 1));
}
}
else
{
back(chars.length);
}
// flush();
}
/**
* 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.
*/
private void clearAhead(final int num) throws IOException
{
if (num == 0)
{
return;
}
if (terminal.isAnsiSupported())
{
printAnsiSequence("K");
return;
}
// print blank extra characters
print((byte) ' ', 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.
*/
private void back(final int num) throws IOException
{
if (num == 0)
return;
if (terminal.isAnsiSupported())
{
int width = getTerminal().getWidth();
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");
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
{
shell.flush();
}
private int backspaceAll() throws IOException
{
return backspace(Integer.MAX_VALUE);
}
/**
* Issue <em>num</em> 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");
}
}
drawBuffer(count);
return count;
}
/**
* Issue a backspace.
*
* @return true if successful
*/
public boolean backspace() throws IOException
{
return backspace(1) == 1;
}
private 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 boolean deleteCurrentCharacter() throws IOException
{
if ((buf.length() == 0) || (buf.cursor == buf.length()))
{
return false;
}
buf.buffer.deleteCharAt(buf.cursor);
drawBuffer(1);
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.current()) && (moveCursor(1) != 0))
{
// nothing
}
while (!isDelimiter(buf.current()) && (moveCursor(1) != 0))
{
// nothing
}
return true;
}
private boolean deletePreviousWord() throws IOException
{
while (isDelimiter(buf.current()) && backspace())
{
// nothing
}
while (!isDelimiter(buf.current()) && backspace())
{
// nothing
}
return true;
}
/**
* Move the cursor <i>where</i> characters.
*
* @param num 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 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 <i>where</i> 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);;
shell.print(new String(chars));
return;
}
else if (buf.cursor == 0)
{
return;
}
else if (mask != null)
{
c = mask;
}
else
{
print(buf.buffer.substring(buf.cursor - where, buf.cursor).getBytes());
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;
}
//
// Key reading
//
/**
* 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);
org.jboss.forge.shell.console.jline.internal.Log.trace("Keystroke: ", c);
// clear any echo characters
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((char) c);
back(num);
drawBuffer(num);
return num;
}
private int countEchoCharacters(final 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
* <p/>
* Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie.
*/
private StringBuilder getPrintableCharacters(final char 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) readVirtualKey()) < 0)
{
// nothing
}
return c;
}
//
// Key Bindings
//
public static final String JLINE_COMPLETION_THRESHOLD = "jline.completion.threshold";
public static final String JLINE_KEYBINDINGS = "jline.keybindings";
public static final String JLINEBINDINGS_PROPERTIES = ".jlinebindings.properties";
/**
* The map for logical operations.
*/
private final short[] keyBindings;
private short[] loadKeyBindings(InputStream input) throws IOException
{
if (input == null)
{
try
{
File file = new File(org.jboss.forge.shell.console.jline.internal.Configuration.getUserHome(),
JLINEBINDINGS_PROPERTIES);
String path = org.jboss.forge.shell.console.jline.internal.Configuration.getString(JLINE_KEYBINDINGS);
if (path != null)
{
file = new File(path);
}
if (file.isFile())
{
org.jboss.forge.shell.console.jline.internal.Log.debug("Loading user bindings from: ", file);
input = new FileInputStream(file);
}
}
catch (Exception e)
{
org.jboss.forge.shell.console.jline.internal.Log.error("Failed to load user bindings", e);
}
}
short[] keyBindings = new short[Character.MAX_VALUE * 2];
Arrays.fill(keyBindings, org.jboss.forge.shell.console.jline.console.Operation.UNKNOWN.code);
if (input == null)
{
org.jboss.forge.shell.console.jline.internal.Log.debug("Using default bindings");
ResourceBundle bundle = getTerminal().getDefaultBindings();
if (bundle == null)
{
throw new RuntimeException("failed to load default keybidings");
}
loadMappingsFromBundle(keyBindings, bundle);
return keyBindings;
}
// Loads the key bindings. Bindings file is in the format:
//
// keycode: operation name
if (input != null)
{
input = new BufferedInputStream(input);
Properties p = new Properties();
p.load(input);
input.close();
for (Object key : p.keySet())
{
String val = (String) key;
try
{
short code = Short.parseShort(val);
String name = p.getProperty(val);
org.jboss.forge.shell.console.jline.console.Operation op = org.jboss.forge.shell.console.jline.console.Operation
.valueOf(name);
keyBindings[code] = op.code;
}
catch (NumberFormatException e)
{
org.jboss.forge.shell.console.jline.internal.Log.error("Failed to convert binding code: ", val, e);
}
}
// hardwired arrow key bindings
// keybindings[VK_UP] = PREV_HISTORY;
// keybindings[VK_DOWN] = NEXT_HISTORY;
// keybindings[VK_LEFT] = PREV_CHAR;
// keybindings[VK_RIGHT] = NEXT_CHAR;
}
return keyBindings;
}
private void loadMappingsFromBundle(final short[] keyBindings, final ResourceBundle bundle)
{
Enumeration<String> keys = bundle.getKeys();
String val;
while (keys.hasMoreElements())
{
val = keys.nextElement();
try
{
short code = Short.parseShort(val);
String name = bundle.getString(val);
org.jboss.forge.shell.console.jline.console.Operation op = org.jboss.forge.shell.console.jline.console.Operation
.valueOf(name);
keyBindings[code] = op.code;
}
catch (NumberFormatException e)
{
org.jboss.forge.shell.console.jline.internal.Log.error("Failed to convert binding code: ", val, e);
}
}
}
int getKeyForAction(final short logicalAction)
{
for (int i = 0; i < keyBindings.length; i++)
{
if (keyBindings[i] == logicalAction)
{
return i;
}
}
return -1;
}
int getKeyForAction(final org.jboss.forge.shell.console.jline.console.Operation op)
{
assert op != null;
return getKeyForAction(op.code);
}
/**
* Reads the console input and returns an array of the form [raw, key binding].
*/
private int[] readBinding() throws IOException
{
int c = readVirtualKey();
for (KeyListener listener : keyListeners)
{
if (listener.keyPress(c))
{
return null;
}
}
if (c == -1)
{
return null;
}
// extract the appropriate key binding
short code = keyBindings[c];
org.jboss.forge.shell.console.jline.internal.Log.trace("Translated: ", c, " -> ", code);
return new int[] { c, code };
}
//
// 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);
}
/**
* 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(String prompt, final Character mask) throws IOException
{
// prompt may be null
// mask may be null
// 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))
{
shell.print(prompt);
shell.flush();
}
// if the terminal is unsupported, just use plain-java reading
if (!terminal.isSupported())
{
return readLine(in);
}
String originalPrompt = this.prompt;
final int NORMAL = 1;
final int SEARCH = 2;
int state = NORMAL;
boolean success = true;
while (true)
{
int[] next = readBinding();
if (next == null)
{
return null;
}
int c = next[0];
// int code = next[1];
org.jboss.forge.shell.console.jline.console.Operation code = org.jboss.forge.shell.console.jline.console.Operation
.valueOf(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)
{
int cursorDest = -1;
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 = searchBackwards(searchTerm.toString());
}
else
{
searchIndex = searchBackwards(searchTerm.toString(), searchIndex);
}
break;
case DELETE_PREV_CHAR:
if (searchTerm.length() > 0)
{
searchTerm.deleteCharAt(searchTerm.length() - 1);
searchIndex = searchBackwards(searchTerm.toString());
}
break;
case UNKNOWN:
searchTerm.appendCodePoint(c);
searchIndex = searchBackwards(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 = NORMAL;
break;
}
// if we're still in search mode, print the search status
if (state == SEARCH)
{
if (searchTerm.length() == 0)
{
printSearchStatus("", "");
searchIndex = -1;
}
else
{
if (searchIndex == -1)
{
beep();
}
else
{
printSearchStatus(searchTerm.toString(), history.get(searchIndex).toString());
}
}
}
// otherwise, restore the line
else
{
restoreLine(originalPrompt, cursorDest);
}
}
if (state == NORMAL)
{
switch (code)
{
case EXIT: // ctrl-d
if (buf.buffer.length() == 0)
{
return null;
}
else
{
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();
// println(); // output newline
// flush();
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.moveToFirst();
if (success)
{
setBuffer(history.current());
}
break;
case END_OF_HISTORY:
success = history.moveToLast();
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 = searchBackwards(searchTerm.toString());
if (searchIndex == -1)
{
beep();
}
printSearchStatus(searchTerm.toString(),
searchIndex > -1 ? history.get(searchIndex).toString() : "");
}
else
{
searchIndex = -1;
printSearchStatus("", "");
}
break;
case UNKNOWN:
default:
if (c != 0)
{ // ignore null chars
ActionListener action = triggeredActions.get((char) c);
if (action != null)
{
action.actionPerformed(null);
}
else
{
putChar(c, true);
}
}
else
{
success = false;
}
}
if (!success)
{
beep();
}
flush();
}
}
}
finally
{
if (!terminal.isSupported())
{
afterReadLine();
}
}
}
/**
* Read a line for unsupported terminals.
*/
private String readLine(final InputStream in) throws IOException
{
StringBuilder buff = new StringBuilder();
while (true)
{
int i = in.read();
if ((i == -1) || (i == '\n') || (i == '\r'))
{
return buff.toString();
}
buff.append((char) i);
}
// return new BufferedReader (new InputStreamReader (in)).readLine ();
}
//
// Completion
//
private final List<org.jboss.forge.shell.console.jline.console.completer.Completer> completers = new LinkedList<org.jboss.forge.shell.console.jline.console.completer.Completer>();
private org.jboss.forge.shell.console.jline.console.completer.CompletionHandler completionHandler = new org.jboss.forge.shell.console.jline.console.completer.CandidateListCompletionHandler();
/**
* Add the specified {@link org.jboss.forge.shell.console.jline.console.completer.Completer} to the list of handlers
* for tab-completion.
*
* @param completer the {@link org.jboss.forge.shell.console.jline.console.completer.Completer} to add
* @return true if it was successfully added
*/
public boolean addCompleter(final org.jboss.forge.shell.console.jline.console.completer.Completer completer)
{
return completers.add(completer);
}
/**
* Remove the specified {@link org.jboss.forge.shell.console.jline.console.completer.Completer} from the list of
* handlers for tab-completion.
*
* @param completer The {@link org.jboss.forge.shell.console.jline.console.completer.Completer} to remove
* @return True if it was successfully removed
*/
public boolean removeCompleter(final org.jboss.forge.shell.console.jline.console.completer.Completer completer)
{
return completers.remove(completer);
}
/**
* Returns an unmodifiable list of all the completers.
*/
public Collection<org.jboss.forge.shell.console.jline.console.completer.Completer> getCompleters()
{
return Collections.unmodifiableList(completers);
}
public void setCompletionHandler(
final org.jboss.forge.shell.console.jline.console.completer.CompletionHandler handler)
{
assert handler != null;
this.completionHandler = handler;
}
public org.jboss.forge.shell.console.jline.console.completer.CompletionHandler getCompletionHandler()
{
return this.completionHandler;
}
/**
* Use the completers to modify the buffer with the appropriate completions.
*
* @return true if successful
*/
private boolean complete() throws IOException
{
// debug ("tab for (" + buf + ")");
if (completers.size() == 0)
{
return false;
}
List<CharSequence> candidates = new LinkedList<CharSequence>();
String bufstr = buf.buffer.toString();
int cursor = buf.cursor;
int position = -1;
for (org.jboss.forge.shell.console.jline.console.completer.Completer comp : completers)
{
if ((position = comp.complete(bufstr, cursor, candidates)) != -1)
{
break;
}
}
return (candidates.size() != 0) && getCompletionHandler().complete(this, candidates, position);
}
/**
* The number of tab-completion candidates above which a warning will be prompted before showing all the candidates.
*/
private int autoprintThreshold = Integer.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 org.jboss.forge.shell.console.jline.console.history.History history = new org.jboss.forge.shell.console.jline.console.history.MemoryHistory();
public void setHistory(final org.jboss.forge.shell.console.jline.console.history.History history)
{
this.history = history;
}
public org.jboss.forge.shell.console.jline.console.history.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;
}
/**
* 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 = System.getProperty("line.separator");
/**
* Output the specified character to the output stream without manipulating the current buffer.
*/
private void print(final int c) throws IOException
{
if (c == '\t')
{
byte[] chars = new byte[TAB_WIDTH];
Arrays.fill(chars, (byte) ' ');
shell.write(chars);
return;
}
shell.write(c);
}
private void print(final char c) throws IOException
{
print((int) c);
}
private void print(final char c, final int i) throws IOException
{
if (i == 1) {
print(c);
}
else {
byte[] chars = new byte[i];
Arrays.fill(chars, (byte) c);
print(chars);
}
}
/**
* Output the specified characters to the output stream without manipulating the current buffer.
*/
private void print(final byte... buff) throws IOException
{
int len = 0;
for (byte c : buff)
{
if (c == '\t')
{
len += TAB_WIDTH;
}
else
{
len++;
}
}
byte[] chars;
if (len == buff.length)
{
chars = buff;
}
else
{
chars = new byte[len];
int pos = 0;
for (byte c : buff)
{
if (c == '\t')
{
Arrays.fill(chars, pos, pos + TAB_WIDTH, (byte) ' ');
pos += TAB_WIDTH;
}
else
{
chars[pos] = c;
pos++;
}
}
}
shell.write(chars);
}
private void print(final byte c, final int num) throws IOException
{
if (num == 1)
{
print(c);
}
else
{
byte[] chars = new byte[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
{
assert s != null;
print(s.toString().getBytes());
}
public final void println(final CharSequence s) throws IOException
{
assert s != null;
print(s.toString().getBytes());
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 <em>num</em> 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);
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, if {@link #isBellEnabled} return true.
*/
public void beep() throws IOException
{
if (isBellEnabled())
{
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)
{
org.jboss.forge.shell.console.jline.internal.Log.error("Paste failed: ", e);
return false;
}
}
//
// Triggered Actions
//
private final Map<Character, ActionListener> triggeredActions = new HashMap<Character, ActionListener>();
/**
* Adding a triggered Action allows to give another curse of action if a character passed the pre-processing.
* <p/>
* 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());
}
org.jboss.forge.shell.console.jline.internal.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 = readVirtualKey();
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 + 3) - 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()
{
@Override
public void run()
{
while (!interrupted())
{
try
{
BufferManager out = getShell().getBufferManager();
out.write(fullPrompt);
out.flushBuffer();
sleep(3);
}
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(final String prompt, final String buffer, int cursorDest) throws IOException
{
// move cursor to end of line
moveToEnd();
// backspace all text, including prompt
buf.buffer.append(this.prompt);
buf.cursor += this.prompt.length();
this.prompt = "";
backspaceAll();
this.prompt = 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(final String searchTerm, final String match) throws IOException
{
String prompt = "(reverse-i-search)`" + searchTerm + "': ";
String buffer = match;
int cursorDest = match.indexOf(searchTerm);
resetPromptLine(prompt, buffer, cursorDest);
}
public void restoreLine(final String originalPrompt, final 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(final String searchTerm, final 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(final String searchTerm)
{
return searchBackwards(searchTerm, history.index());
}
public int searchBackwards(final String searchTerm, final int startIndex, final boolean startsWith)
{
ListIterator<org.jboss.forge.shell.console.jline.console.history.History.Entry> it = history.entries(startIndex);
while (it.hasPrevious())
{
org.jboss.forge.shell.console.jline.console.history.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;
}
//
// 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);
}
private static final String ESCAPE_STR = new String(new char[] { 27, '[' });
private void printAnsiSequence(final String sequence) throws IOException
{
print(ESCAPE_STR + sequence);
}
// return column position, reported by the terminal
private int getCurrentPosition()
{
// check for ByteArrayInputStream to disable for unit tests
if (terminal.isAnsiSupported() && !(in instanceof ByteArrayInputStream))
{
try
{
printAnsiSequence("6n");
flush();
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(";");
return Integer.parseInt(pos[1]);
}
catch (Exception x)
{
// no luck
}
}
return -1; // TODO: throw exception instead?
}
}