/* * Copyright (c) 2002-2012, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. * * http://www.opensource.org/licenses/bsd-license.php */ package jdk.internal.jline.console; import java.awt.*; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ActionListener; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.ResourceBundle; import java.util.Stack; import java.util.regex.Pattern; import jdk.internal.jline.Terminal; import jdk.internal.jline.TerminalFactory; import jdk.internal.jline.UnixTerminal; import jdk.internal.jline.console.completer.CandidateListCompletionHandler; import jdk.internal.jline.console.completer.Completer; import jdk.internal.jline.console.completer.CompletionHandler; import jdk.internal.jline.console.history.History; import jdk.internal.jline.console.history.MemoryHistory; import jdk.internal.jline.internal.Configuration; import jdk.internal.jline.internal.InputStreamReader; import jdk.internal.jline.internal.Log; import jdk.internal.jline.internal.NonBlockingInputStream; import jdk.internal.jline.internal.Nullable; import jdk.internal.jline.internal.Urls; //import org.fusesource.jansi.AnsiOutputStream; import static jdk.internal.jline.internal.Preconditions.checkNotNull; /** * A reader for console applications. It supports custom tab-completion, * saveable command history, and command line editing. On some platforms, * platform-specific commands will need to be issued before the reader will * function properly. See {@link jline.Terminal#init} for convenience * methods for issuing platform-specific setup commands. * * @author Marc Prud'hommeaux * @author Jason Dillon * @author Guillaume Nodet */ public class ConsoleReader { public static final String JLINE_NOBELL = "jline.nobell"; public static final String JLINE_ESC_TIMEOUT = "jline.esc.timeout"; public static final String JLINE_INPUTRC = "jline.inputrc"; public static final String INPUT_RC = ".inputrc"; public static final String DEFAULT_INPUT_RC = "/etc/inputrc"; public static final char BACKSPACE = '\b'; public static final char RESET_LINE = '\r'; public static final char KEYBOARD_BELL = '\07'; public static final char NULL_MASK = 0; public static final int TAB_WIDTH = 4; private static final ResourceBundle resources = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName()); private final Terminal terminal; private final Writer out; private final CursorBuffer buf = new CursorBuffer(); private String prompt; private int promptLen; private boolean expandEvents = true; private boolean bellEnabled = !Configuration.getBoolean(JLINE_NOBELL, true); private boolean handleUserInterrupt = false; private Character mask; private Character echoCharacter; private StringBuffer searchTerm = null; private String previousSearchTerm = ""; private int searchIndex = -1; private int parenBlinkTimeout = 500; /* * The reader and the nonBlockingInput go hand-in-hand. The reader wraps * the nonBlockingInput, but we have to retain a handle to it so that * we can shut down its blocking read thread when we go away. */ private NonBlockingInputStream in; private long escapeTimeout; private Reader reader; /* * TODO: Please read the comments about this in setInput(), but this needs * to be done away with. */ private boolean isUnitTestInput; /** * Last character searched for with a vi character search */ private char charSearchChar = 0; // Character to search for private char charSearchLastInvokeChar = 0; // Most recent invocation key private char charSearchFirstInvokeChar = 0;// First character that invoked /** * The vi yank buffer */ private String yankBuffer = ""; private KillRing killRing = new KillRing(); private String encoding; private boolean recording; private String macro = ""; private String appName; private URL inputrcUrl; private ConsoleKeys consoleKeys; private String commentBegin = null; private boolean skipLF = false; /** * Set to true if the reader should attempt to detect copy-n-paste. The * effect of this that an attempt is made to detect if tab is quickly * followed by another character, then it is assumed that the tab was * a literal tab as part of a copy-and-paste operation and is inserted as * such. */ private boolean copyPasteDetection = false; /* * Current internal state of the line reader */ private State state = State.NORMAL; /** * Possible states in which the current readline operation may be in. */ private static enum State { /** * The user is just typing away */ NORMAL, /** * In the middle of a emacs seach */ SEARCH, FORWARD_SEARCH, /** * VI "yank-to" operation ("y" during move mode) */ VI_YANK_TO, /** * VI "delete-to" operation ("d" during move mode) */ VI_DELETE_TO, /** * VI "change-to" operation ("c" during move mode) */ VI_CHANGE_TO } public ConsoleReader() throws IOException { this(null, new FileInputStream(FileDescriptor.in), System.out, null); } public ConsoleReader(final InputStream in, final OutputStream out) throws IOException { this(null, in, out, null); } public ConsoleReader(final InputStream in, final OutputStream out, final Terminal term) throws IOException { this(null, in, out, term); } public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term) throws IOException { this(appName, in, out, term, null); } public ConsoleReader(final @Nullable String appName, final InputStream in, final OutputStream out, final @Nullable Terminal term, final @Nullable String encoding) throws IOException { this.appName = appName != null ? appName : "JLine"; this.encoding = encoding != null ? encoding : Configuration.getEncoding(); this.terminal = term != null ? term : TerminalFactory.get(); String outEncoding = terminal.getOutputEncoding() != null? terminal.getOutputEncoding() : this.encoding; this.out = new OutputStreamWriter(terminal.wrapOutIfNeeded(out), outEncoding); setInput( in ); this.inputrcUrl = getInputRc(); consoleKeys = new ConsoleKeys(this.appName, inputrcUrl); } private URL getInputRc() throws IOException { String path = Configuration.getString(JLINE_INPUTRC); if (path == null) { File f = new File(Configuration.getUserHome(), INPUT_RC); if (!f.exists()) { f = new File(DEFAULT_INPUT_RC); } return f.toURI().toURL(); } else { return Urls.create(path); } } public KeyMap getKeys() { return consoleKeys.getKeys(); } void setInput(final InputStream in) throws IOException { this.escapeTimeout = Configuration.getLong(JLINE_ESC_TIMEOUT, 100); /* * This is gross and here is how to fix it. In getCurrentPosition() * and getCurrentAnsiRow(), the logic is disabled when running unit * tests and the fact that it is a unit test is determined by knowing * if the original input stream was a ByteArrayInputStream. So, this * is our test to do this. What SHOULD happen is that the unit * tests should pass in a terminal that is appropriately configured * such that whatever behavior they expect to happen (or not happen) * happens (or doesn't). * * So, TODO, get rid of this and fix the unit tests. */ this.isUnitTestInput = in instanceof ByteArrayInputStream; boolean nonBlockingEnabled = escapeTimeout > 0L && terminal.isSupported() && in != null; /* * If we had a non-blocking thread already going, then shut it down * and start a new one. */ if (this.in != null) { this.in.shutdown(); } final InputStream wrapped = terminal.wrapInIfNeeded( in ); this.in = new NonBlockingInputStream(wrapped, nonBlockingEnabled); this.reader = new InputStreamReader( this.in, encoding ); } /** * Shuts the console reader down. This method should be called when you * have completed using the reader as it shuts down and cleans up resources * that would otherwise be "leaked". */ public void shutdown() { if (in != null) { in.shutdown(); } } /** * Shuts down the ConsoleReader if the JVM attempts to clean it up. */ @Override protected void finalize() throws Throwable { try { shutdown(); } finally { super.finalize(); } } public InputStream getInput() { return in; } public Writer getOutput() { return out; } public Terminal getTerminal() { return terminal; } public CursorBuffer getCursorBuffer() { return buf; } public void setExpandEvents(final boolean expand) { this.expandEvents = expand; } public boolean getExpandEvents() { return expandEvents; } /** * Enables or disables copy and paste detection. The effect of enabling this * this setting is that when a tab is received immediately followed by another * character, the tab will not be treated as a completion, but as a tab literal. * @param onoff true if detection is enabled */ public void setCopyPasteDetection(final boolean onoff) { copyPasteDetection = onoff; } /** * @return true if copy and paste detection is enabled. */ public boolean isCopyPasteDetectionEnabled() { return copyPasteDetection; } /** * Set whether the console bell is enabled. * * @param enabled true if enabled; false otherwise * @since 2.7 */ public void setBellEnabled(boolean enabled) { this.bellEnabled = enabled; } /** * Get whether the console bell is enabled * * @return true if enabled; false otherwise * @since 2.7 */ public boolean getBellEnabled() { return bellEnabled; } /** * Set whether user interrupts (ctrl-C) are handled by having JLine * throw {@link UserInterruptException} from {@link #readLine}. * Otherwise, the JVM will handle {@code SIGINT} as normal, which * usually causes it to exit. The default is {@code false}. * * @since 2.10 */ public void setHandleUserInterrupt(boolean enabled) { this.handleUserInterrupt = enabled; } /** * Get whether user interrupt handling is enabled * * @return true if enabled; false otherwise * @since 2.10 */ public boolean getHandleUserInterrupt() { return handleUserInterrupt; } /** * Sets the string that will be used to start a comment when the * insert-comment key is struck. * @param commentBegin The begin comment string. * @since 2.7 */ public void setCommentBegin(String commentBegin) { this.commentBegin = commentBegin; } /** * @return the string that will be used to start a comment when the * insert-comment key is struck. * @since 2.7 */ public String getCommentBegin() { String str = commentBegin; if (str == null) { str = consoleKeys.getVariable("comment-begin"); if (str == null) { str = "#"; } } return str; } public void setPrompt(final String prompt) { this.prompt = prompt; this.promptLen = ((prompt == null) ? 0 : stripAnsi(lastLine(prompt)).length()); } public String getPrompt() { return prompt; } /** * Set the echo character. For example, to have "*" entered when a password is typed: *
** myConsoleReader.setEchoCharacter(new Character('*')); ** * Setting the character to * *
* null ** * will restore normal character echoing. Setting the character to * *
* new Character(0) ** * will cause nothing to be echoed. * * @param c the character to echo to the console in place of the typed character. */ public void setEchoCharacter(final Character c) { this.echoCharacter = c; } /** * Returns the echo character. */ public Character getEchoCharacter() { return echoCharacter; } /** * Erase the current line. * * @return false if we failed (e.g., the buffer was empty) */ protected final boolean resetLine() throws IOException { if (buf.cursor == 0) { return false; } StringBuilder killed = new StringBuilder(); while (buf.cursor > 0) { char c = buf.current(); if (c == 0) { break; } killed.append(c); backspace(); } String copy = killed.reverse().toString(); killRing.addBackwards(copy); return true; } int getCursorPosition() { // FIXME: does not handle anything but a line with a prompt absolute position return promptLen + buf.cursor; } /** * Returns the text after the last '\n'. * prompt is returned if no '\n' characters are present. * null is returned if prompt is null. */ private String lastLine(String str) { if (str == null) return ""; int last = str.lastIndexOf("\n"); if (last >= 0) { return str.substring(last + 1, str.length()); } return str; } String stripAnsi(String str) { if (str == null) return ""; return ANSI_CODE_PATTERN.matcher(str).replaceAll(""); // try { // ByteArrayOutputStream baos = new ByteArrayOutputStream(); // AnsiOutputStream aos = new AnsiOutputStream(baos); // aos.write(str.getBytes()); // aos.flush(); // return baos.toString(); // } catch (IOException e) { // return str; // } } //where: private static final Pattern ANSI_CODE_PATTERN = Pattern.compile("\033\\[[^@-~]*[@-~]"); /** * Move the cursor position to the specified absolute index. */ public final boolean setCursorPosition(final int position) throws IOException { if (position == buf.cursor) { return true; } return moveCursor(position - buf.cursor) != 0; } /** * Set the current buffer's content to the specified {@link String}. The * visual console will be modified to show the current buffer. * * @param buffer the new contents of the buffer. */ private void setBuffer(final String buffer) throws IOException { // don't bother modifying it if it is unchanged if (buffer.equals(buf.buffer.toString())) { return; } // obtain the difference between the current buffer and the new one int sameIndex = 0; for (int i = 0, l1 = buffer.length(), l2 = buf.buffer.length(); (i < l1) && (i < l2); i++) { if (buffer.charAt(i) == buf.buffer.charAt(i)) { sameIndex++; } else { break; } } int diff = buf.cursor - sameIndex; if (diff < 0) { // we can't backspace here so try from the end of the buffer moveToEnd(); diff = buf.buffer.length() - sameIndex; } backspace(diff); // go back for the differences killLine(); // clear to the end of the line buf.buffer.setLength(sameIndex); // the new length putString(buffer.substring(sameIndex)); // append the differences } private void setBuffer(final CharSequence buffer) throws IOException { setBuffer(String.valueOf(buffer)); } private void setBufferKeepPos(final String buffer) throws IOException { int pos = buf.cursor; setBuffer(buffer); setCursorPosition(pos); } private void setBufferKeepPos(final CharSequence buffer) throws IOException { setBufferKeepPos(String.valueOf(buffer)); } /** * Output put the prompt + the current buffer */ public final void drawLine() throws IOException { String prompt = getPrompt(); if (prompt != null) { print(prompt); } print(buf.buffer.toString()); if (buf.length() != buf.cursor) { // not at end of line back(buf.length() - buf.cursor - 1); } // force drawBuffer to check for weird wrap (after clear screen) drawBuffer(); } /** * Clear the line and redraw it. */ public final void redrawLine() throws IOException { print(RESET_LINE); // flush(); drawLine(); } /** * Clear the buffer and add its contents to the history. * * @return the former contents of the buffer. */ final String finishBuffer() throws IOException { // FIXME: Package protected because used by tests String str = buf.buffer.toString(); String historyLine = str; if (expandEvents) { try { str = expandEvents(str); // all post-expansion occurrences of '!' must have been escaped, so re-add escape to each historyLine = str.replace("!", "\\!"); // only leading '^' results in expansion, so only re-add escape for that case historyLine = historyLine.replaceAll("^\\^", "\\\\^"); } catch(IllegalArgumentException e) { Log.error("Could not expand event", e); beep(); buf.clear(); 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(historyLine); } else { mask = null; } } history.moveToEnd(); buf.buffer.setLength(0); buf.cursor = 0; return str; } /** * Expand event designator such as !!, !#, !3, etc... * See http://www.gnu.org/software/bash/manual/html_node/Event-Designators.html */ @SuppressWarnings("fallthrough") protected String expandEvents(String str) throws IOException { StringBuilder sb = new StringBuilder(); for (int i = 0; i < str.length(); i++) { char c = str.charAt(i); switch (c) { case '\\': // any '\!' should be considered an expansion escape, so skip expansion and strip the escape character // a leading '\^' should be considered an expansion escape, so skip expansion and strip the escape character // otherwise, add the escape if (i + 1 < str.length()) { char nextChar = str.charAt(i+1); if (nextChar == '!' || (nextChar == '^' && i == 0)) { c = nextChar; i++; } } sb.append(c); break; case '!': if (i + 1 < str.length()) { c = str.charAt(++i); boolean neg = false; String rep = null; int i1, idx; switch (c) { case '!': if (history.size() == 0) { throw new IllegalArgumentException("!!: event not found"); } rep = history.get(history.index() - 1).toString(); break; case '#': sb.append(sb.toString()); break; case '?': i1 = str.indexOf('?', i + 1); if (i1 < 0) { i1 = str.length(); } String sc = str.substring(i + 1, i1); i = i1; idx = searchBackwards(sc); if (idx < 0) { throw new IllegalArgumentException("!?" + sc + ": event not found"); } else { rep = history.get(idx).toString(); } break; case '$': if (history.size() == 0) { throw new IllegalArgumentException("!$: event not found"); } String previous = history.get(history.index() - 1).toString().trim(); int lastSpace = previous.lastIndexOf(' '); if(lastSpace != -1) { rep = previous.substring(lastSpace+1); } else { rep = previous; } break; case ' ': case '\t': sb.append('!'); sb.append(c); break; case '-': neg = true; i++; // fall through case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': i1 = i; for (; i < str.length(); i++) { c = str.charAt(i); if (c < '0' || c > '9') { break; } } idx = 0; try { idx = Integer.parseInt(str.substring(i1, i)); } catch (NumberFormatException e) { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } if (neg) { if (idx > 0 && idx <= history.size()) { rep = (history.get(history.index() - idx)).toString(); } else { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } } else { if (idx > history.index() - history.size() && idx <= history.index()) { rep = (history.get(idx - 1)).toString(); } else { throw new IllegalArgumentException((neg ? "!-" : "!") + str.substring(i1, i) + ": event not found"); } } break; default: String ss = str.substring(i); i = str.length(); idx = searchBackwards(ss, history.index(), true); if (idx < 0) { throw new IllegalArgumentException("!" + ss + ": event not found"); } else { rep = history.get(idx).toString(); } break; } if (rep != null) { sb.append(rep); } } else { sb.append(c); } break; case '^': if (i == 0) { int i1 = str.indexOf('^', i + 1); int i2 = str.indexOf('^', i1 + 1); if (i2 < 0) { i2 = str.length(); } if (i1 > 0 && i2 > 0) { String s1 = str.substring(i + 1, i1); String s2 = str.substring(i1 + 1, i2); String s = history.get(history.index() - 1).toString().replace(s1, s2); sb.append(s); i = i2 + 1; break; } } sb.append(c); break; default: sb.append(c); break; } } String result = sb.toString(); if (!str.equals(result)) { print(result); println(); flush(); } return result; } /** * Write out the specified string to the buffer and the output stream. */ public final void putString(final CharSequence str) throws IOException { buf.write(str); if (mask == null) { // no masking print(str); } else if (mask == NULL_MASK) { // don't print anything } else { print(mask, str.length()); } drawBuffer(); } /** * Redraw the rest of the buffer from the cursor onwards. This is necessary * for inserting text into the buffer. * * @param clear the number of characters to clear after the end of the buffer */ private void drawBuffer(final int clear) throws IOException { // debug ("drawBuffer: " + clear); if (buf.cursor == buf.length() && clear == 0) { } else { char[] chars = buf.buffer.substring(buf.cursor).toCharArray(); if (mask != null) { Arrays.fill(chars, mask); } if (terminal.hasWeirdWrap()) { // need to determine if wrapping will occur: int width = terminal.getWidth(); int pos = getCursorPosition(); for (int i = 0; i < chars.length; i++) { print(chars[i]); if ((pos + i + 1) % width == 0) { print(32); // move cursor to next line by printing dummy space print(13); // CR / not newline. } } } else { print(chars); } clearAhead(clear, chars.length); if (terminal.isAnsiSupported()) { if (chars.length > 0) { back(chars.length); } } else { back(chars.length); } } if (terminal.hasWeirdWrap()) { int width = terminal.getWidth(); // best guess on whether the cursor is in that weird location... // Need to do this without calling ansi cursor location methods // otherwise it breaks paste of wrapped lines in xterm. if (getCursorPosition() > 0 && (getCursorPosition() % width == 0) && buf.cursor == buf.length() && clear == 0) { // the following workaround is reverse-engineered from looking // at what bash sent to the terminal in the same situation print(32); // move cursor to next line by printing dummy space print(13); // CR / not newline. } } } /** * Redraw the rest of the buffer from the cursor onwards. This is necessary * for inserting text into the buffer. */ private void drawBuffer() throws IOException { drawBuffer(0); } /** * Clear ahead the specified number of characters without moving the cursor. * * @param num the number of characters to clear * @param delta the difference between the internal cursor and the screen * cursor - if > 0, assume some stuff was printed and weird wrap has to be * checked */ private void clearAhead(final int num, int delta) throws IOException { if (num == 0) { return; } if (terminal.isAnsiSupported()) { int width = terminal.getWidth(); int screenCursorCol = getCursorPosition() + delta; // clear current line printAnsiSequence("K"); // if cursor+num wraps, then we need to clear the line(s) below too int curCol = screenCursorCol % width; int endCol = (screenCursorCol + num - 1) % width; int lines = num / width; if (endCol < curCol) lines++; for (int i = 0; i < lines; i++) { printAnsiSequence("B"); printAnsiSequence("2K"); } for (int i = 0; i < lines; i++) { printAnsiSequence("A"); } return; } // print blank extra characters print(' ', num); // we need to flush here so a "clever" console doesn't just ignore the redundancy // of a space followed by a backspace. // flush(); // reset the visual cursor back(num); // flush(); } /** * Move the visual cursor backwards without modifying the buffer cursor. */ protected void back(final int num) throws IOException { if (num == 0) return; if (terminal.isAnsiSupported()) { int width = getTerminal().getWidth(); int cursor = getCursorPosition(); int realCursor = cursor + num; int realCol = realCursor % width; int newCol = cursor % width; int moveup = num / width; int delta = realCol - newCol; if (delta < 0) moveup++; if (moveup > 0) { printAnsiSequence(moveup + "A"); } printAnsiSequence((1 + newCol) + "G"); return; } print(BACKSPACE, num); // flush(); } /** * Flush the console output stream. This is important for printout out single characters (like a backspace or * keyboard) that we want the console to handle immediately. */ public void flush() throws IOException { out.flush(); } private int backspaceAll() throws IOException { return backspace(Integer.MAX_VALUE); } /** * Issue num backspaces. * * @return the number of characters backed up */ private int backspace(final int num) throws IOException { if (buf.cursor == 0) { return 0; } int count = 0; int termwidth = getTerminal().getWidth(); int lines = getCursorPosition() / termwidth; count = moveCursor(-1 * num) * -1; buf.buffer.delete(buf.cursor, buf.cursor + count); if (getCursorPosition() / termwidth != lines) { if (terminal.isAnsiSupported()) { // debug("doing backspace redraw: " + getCursorPosition() + " on " + termwidth + ": " + lines); printAnsiSequence("K"); // if cursor+num wraps, then we need to clear the line(s) below too // last char printed is one pos less than cursor so we subtract // one /* // TODO: fixme (does not work - test with reverse search with wrapping line and CTRL-E) int endCol = (getCursorPosition() + num - 1) % termwidth; int curCol = getCursorPosition() % termwidth; if (endCol < curCol) lines++; for (int i = 1; i < lines; i++) { printAnsiSequence("B"); printAnsiSequence("2K"); } for (int i = 1; i < lines; i++) { printAnsiSequence("A"); } return count; */ } } drawBuffer(count); return count; } /** * Issue a backspace. * * @return true if successful */ public boolean backspace() throws IOException { return backspace(1) == 1; } protected boolean moveToEnd() throws IOException { if (buf.cursor == buf.length()) { return true; } return moveCursor(buf.length() - buf.cursor) > 0; } /** * Delete the character at the current position and redraw the remainder of the buffer. */ private boolean deleteCurrentCharacter() throws IOException { if (buf.length() == 0 || buf.cursor == buf.length()) { return false; } buf.buffer.deleteCharAt(buf.cursor); drawBuffer(1); return true; } /** * This method is calling while doing a delete-to ("d"), change-to ("c"), * or yank-to ("y") and it filters out only those movement operations * that are allowable during those operations. Any operation that isn't * allow drops you back into movement mode. * * @param op The incoming operation to remap * @return The remaped operation */ private Operation viDeleteChangeYankToRemap (Operation op) { switch (op) { case VI_EOF_MAYBE: case ABORT: case BACKWARD_CHAR: case FORWARD_CHAR: case END_OF_LINE: case VI_MATCH: case VI_BEGNNING_OF_LINE_OR_ARG_DIGIT: case VI_ARG_DIGIT: case VI_PREV_WORD: case VI_END_WORD: case VI_CHAR_SEARCH: case VI_NEXT_WORD: case VI_FIRST_PRINT: case VI_GOTO_MARK: case VI_COLUMN: case VI_DELETE_TO: case VI_YANK_TO: case VI_CHANGE_TO: return op; default: return Operation.VI_MOVEMENT_MODE; } } /** * Deletes the previous character from the cursor position * @param count number of times to do it. * @return true if it was done. * @throws IOException */ private boolean viRubout(int count) throws IOException { boolean ok = true; for (int i = 0; ok && i < count; i++) { ok = backspace(); } return ok; } /** * Deletes the character you are sitting on and sucks the rest of * the line in from the right. * @param count Number of times to perform the operation. * @return true if its works, false if it didn't * @throws IOException */ private boolean viDelete(int count) throws IOException { boolean ok = true; for (int i = 0; ok && i < count; i++) { ok = deleteCurrentCharacter(); } return ok; } /** * Switches the case of the current character from upper to lower * or lower to upper as necessary and advances the cursor one * position to the right. * @param count The number of times to repeat * @return true if it completed successfully, false if not all * case changes could be completed. * @throws IOException */ private boolean viChangeCase(int count) throws IOException { boolean ok = true; for (int i = 0; ok && i < count; i++) { ok = buf.cursor < buf.buffer.length (); if (ok) { char ch = buf.buffer.charAt(buf.cursor); if (Character.isUpperCase(ch)) { ch = Character.toLowerCase(ch); } else if (Character.isLowerCase(ch)) { ch = Character.toUpperCase(ch); } buf.buffer.setCharAt(buf.cursor, ch); drawBuffer(1); moveCursor(1); } } return ok; } /** * Implements the vi change character command (in move-mode "r" * followed by the character to change to). * @param count Number of times to perform the action * @param c The character to change to * @return Whether or not there were problems encountered * @throws IOException */ private boolean viChangeChar(int count, int c) throws IOException { // EOF, ESC, or CTRL-C aborts. if (c < 0 || c == '\033' || c == '\003') { return true; } boolean ok = true; for (int i = 0; ok && i < count; i++) { ok = buf.cursor < buf.buffer.length (); if (ok) { buf.buffer.setCharAt(buf.cursor, (char) c); drawBuffer(1); if (i < (count-1)) { moveCursor(1); } } } return ok; } /** * This is a close facsimile of the actual vi previous word logic. In * actual vi words are determined by boundaries of identity characterse. * This logic is a bit more simple and simply looks at white space or * digits or characters. It should be revised at some point. * * @param count number of iterations * @return true if the move was successful, false otherwise * @throws IOException */ private boolean viPreviousWord(int count) throws IOException { boolean ok = true; if (buf.cursor == 0) { return false; } int pos = buf.cursor - 1; for (int i = 0; pos > 0 && i < count; i++) { // If we are on white space, then move back. while (pos > 0 && isWhitespace(buf.buffer.charAt(pos))) { --pos; } while (pos > 0 && !isDelimiter(buf.buffer.charAt(pos-1))) { --pos; } if (pos > 0 && i < (count-1)) { --pos; } } setCursorPosition(pos); return ok; } /** * Performs the vi "delete-to" action, deleting characters between a given * span of the input line. * @param startPos The start position * @param endPos The end position. * @param isChange If true, then the delete is part of a change operationg * (e.g. "c$" is change-to-end-of line, so we first must delete to end * of line to start the change * @return true if it succeeded, false otherwise * @throws IOException */ private boolean viDeleteTo(int startPos, int endPos, boolean isChange) throws IOException { if (startPos == endPos) { return true; } if (endPos < startPos) { int tmp = endPos; endPos = startPos; startPos = tmp; } setCursorPosition(startPos); buf.cursor = startPos; buf.buffer.delete(startPos, endPos); drawBuffer(endPos - startPos); // If we are doing a delete operation (e.g. "d$") then don't leave the // cursor dangling off the end. In reality the "isChange" flag is silly // what is really happening is that if we are in "move-mode" then the // cursor can't be moved off the end of the line, but in "edit-mode" it // is ok, but I have no easy way of knowing which mode we are in. if (! isChange && startPos > 0 && startPos == buf.length()) { moveCursor(-1); } return true; } /** * Implement the "vi" yank-to operation. This operation allows you * to yank the contents of the current line based upon a move operation, * for exaple "yw" yanks the current word, "3yw" yanks 3 words, etc. * * @param startPos The starting position from which to yank * @param endPos The ending position to which to yank * @return true if the yank succeeded * @throws IOException */ private boolean viYankTo(int startPos, int endPos) throws IOException { int cursorPos = startPos; if (endPos < startPos) { int tmp = endPos; endPos = startPos; startPos = tmp; } if (startPos == endPos) { yankBuffer = ""; return true; } yankBuffer = buf.buffer.substring(startPos, endPos); /* * It was a movement command that moved the cursor to find the * end position, so put the cursor back where it started. */ setCursorPosition(cursorPos); return true; } /** * Pasts the yank buffer to the right of the current cursor position * and moves the cursor to the end of the pasted region. * * @param count Number of times to perform the operation. * @return true if it worked, false otherwise * @throws IOException */ private boolean viPut(int count) throws IOException { if (yankBuffer.length () == 0) { return true; } if (buf.cursor < buf.buffer.length ()) { moveCursor(1); } for (int i = 0; i < count; i++) { putString(yankBuffer); } moveCursor(-1); return true; } /** * Searches forward of the current position for a character and moves * the cursor onto it. * @param count Number of times to repeat the process. * @param ch The character to search for * @return true if the char was found, false otherwise * @throws IOException */ private boolean viCharSearch(int count, int invokeChar, int ch) throws IOException { if (ch < 0 || invokeChar < 0) { return false; } char searchChar = (char)ch; boolean isForward; boolean stopBefore; /* * The character stuff turns out to be hairy. Here is how it works: * f - search forward for ch * F - search backward for ch * t - search forward for ch, but stop just before the match * T - search backward for ch, but stop just after the match * ; - After [fFtT;], repeat the last search, after ',' reverse it * , - After [fFtT;], reverse the last search, after ',' repeat it */ if (invokeChar == ';' || invokeChar == ',') { // No recent search done? Then bail if (charSearchChar == 0) { return false; } // Reverse direction if switching between ',' and ';' if (charSearchLastInvokeChar == ';' || charSearchLastInvokeChar == ',') { if (charSearchLastInvokeChar != invokeChar) { charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar); } } else { if (invokeChar == ',') { charSearchFirstInvokeChar = switchCase(charSearchFirstInvokeChar); } } searchChar = charSearchChar; } else { charSearchChar = searchChar; charSearchFirstInvokeChar = (char) invokeChar; } charSearchLastInvokeChar = (char)invokeChar; isForward = Character.isLowerCase(charSearchFirstInvokeChar); stopBefore = (Character.toLowerCase(charSearchFirstInvokeChar) == 't'); boolean ok = false; if (isForward) { while (count-- > 0) { int pos = buf.cursor + 1; while (pos < buf.buffer.length()) { if (buf.buffer.charAt(pos) == searchChar) { setCursorPosition(pos); ok = true; break; } ++pos; } } if (ok) { if (stopBefore) moveCursor(-1); /* * When in yank-to, move-to, del-to state we actually want to * go to the character after the one we landed on to make sure * that the character we ended up on is included in the * operation */ if (isInViMoveOperationState()) { moveCursor(1); } } } else { while (count-- > 0) { int pos = buf.cursor - 1; while (pos >= 0) { if (buf.buffer.charAt(pos) == searchChar) { setCursorPosition(pos); ok = true; break; } --pos; } } if (ok && stopBefore) moveCursor(1); } return ok; } private char switchCase(char ch) { if (Character.isUpperCase(ch)) { return Character.toLowerCase(ch); } return Character.toUpperCase(ch); } /** * @return true if line reader is in the middle of doing a change-to * delete-to or yank-to. */ private final boolean isInViMoveOperationState() { return state == State.VI_CHANGE_TO || state == State.VI_DELETE_TO || state == State.VI_YANK_TO; } /** * This is a close facsimile of the actual vi next word logic. * As with viPreviousWord() this probably needs to be improved * at some point. * * @param count number of iterations * @return true if the move was successful, false otherwise * @throws IOException */ private boolean viNextWord(int count) throws IOException { int pos = buf.cursor; int end = buf.buffer.length(); for (int i = 0; pos < end && i < count; i++) { // Skip over letter/digits while (pos < end && !isDelimiter(buf.buffer.charAt(pos))) { ++pos; } /* * Don't you love special cases? During delete-to and yank-to * operations the word movement is normal. However, during a * change-to, the trailing spaces behind the last word are * left in tact. */ if (i < (count-1) || !(state == State.VI_CHANGE_TO)) { while (pos < end && isDelimiter(buf.buffer.charAt(pos))) { ++pos; } } } setCursorPosition(pos); return true; } /** * Implements a close facsimile of the vi end-of-word movement. * If the character is on white space, it takes you to the end * of the next word. If it is on the last character of a word * it takes you to the next of the next word. Any other character * of a word, takes you to the end of the current word. * * @param count Number of times to repeat the action * @return true if it worked. * @throws IOException */ private boolean viEndWord(int count) throws IOException { int pos = buf.cursor; int end = buf.buffer.length(); for (int i = 0; pos < end && i < count; i++) { if (pos < (end-1) && !isDelimiter(buf.buffer.charAt(pos)) && isDelimiter(buf.buffer.charAt (pos+1))) { ++pos; } // If we are on white space, then move back. while (pos < end && isDelimiter(buf.buffer.charAt(pos))) { ++pos; } while (pos < (end-1) && !isDelimiter(buf.buffer.charAt(pos+1))) { ++pos; } } setCursorPosition(pos); return true; } private boolean previousWord() throws IOException { while (isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { // nothing } while (!isDelimiter(buf.current()) && (moveCursor(-1) != 0)) { // nothing } return true; } private boolean nextWord() throws IOException { while (isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) { // nothing } while (!isDelimiter(buf.nextChar()) && (moveCursor(1) != 0)) { // nothing } return true; } /** * Deletes to the beginning of the word that the cursor is sitting on. * If the cursor is on white-space, it deletes that and to the beginning * of the word before it. If the user is not on a word or whitespace * it deletes up to the end of the previous word. * * @param count Number of times to perform the operation * @return true if it worked, false if you tried to delete too many words * @throws IOException */ private boolean unixWordRubout(int count) throws IOException { boolean success = true; StringBuilder killed = new StringBuilder(); for (; count > 0; --count) { if (buf.cursor == 0) { success = false; break; } while (isWhitespace(buf.current())) { char c = buf.current(); if (c == 0) { break; } killed.append(c); backspace(); } while (!isWhitespace(buf.current())) { char c = buf.current(); if (c == 0) { break; } killed.append(c); backspace(); } } String copy = killed.reverse().toString(); killRing.addBackwards(copy); return success; } private String insertComment(boolean isViMode) throws IOException { String comment = this.getCommentBegin (); setCursorPosition(0); putString(comment); if (isViMode) { consoleKeys.setKeyMap(KeyMap.VI_INSERT); } return accept(); } /** * Similar to putString() but allows the string to be repeated a specific * number of times, allowing easy support of vi digit arguments to a given * command. The string is placed as the current cursor position. * * @param count The count of times to insert the string. * @param str The string to insert * @return true if the operation is a success, false otherwise * @throws IOException */ private boolean insert(int count, final CharSequence str) throws IOException { for (int i = 0; i < count; i++) { buf.write(str); if (mask == null) { // no masking print(str); } else if (mask == NULL_MASK) { // don't print anything } else { print(mask, str.length()); } } drawBuffer(); return true; } /** * Implements vi search ("/" or "?"). * @throws IOException */ @SuppressWarnings("fallthrough") private int viSearch(char searchChar) throws IOException { boolean isForward = (searchChar == '/'); /* * This is a little gross, I'm sure there is a more appropriate way * of saving and restoring state. */ CursorBuffer origBuffer = buf.copy(); // Clear the contents of the current line and setCursorPosition (0); killLine(); // Our new "prompt" is the character that got us into search mode. putString(Character.toString(searchChar)); flush(); boolean isAborted = false; boolean isComplete = false; /* * Readline doesn't seem to do any special character map handling * here, so I think we are safe. */ int ch = -1; while (!isAborted && !isComplete && (ch = readCharacter()) != -1) { switch (ch) { case '\033': // ESC /* * The ESC behavior doesn't appear to be readline behavior, * but it is a little tweak of my own. I like it. */ isAborted = true; break; case '\010': // Backspace case '\177': // Delete backspace(); /* * Backspacing through the "prompt" aborts the search. */ if (buf.cursor == 0) { isAborted = true; } break; case '\012': // NL case '\015': // CR isComplete = true; break; default: putString(Character.toString((char) ch)); } flush(); } // If we aborted, then put ourself at the end of the original buffer. if (ch == -1 || isAborted) { setCursorPosition(0); killLine(); putString(origBuffer.buffer); setCursorPosition(origBuffer.cursor); return -1; } /* * The first character of the buffer was the search character itself * so we discard it. */ String searchTerm = buf.buffer.substring(1); int idx = -1; /* * The semantics of the history thing is gross when you want to * explicitly iterate over entries (without an iterator) as size() * returns the actual number of entries in the list but get() * doesn't work the way you think. */ int end = history.index(); int start = (end <= history.size()) ? 0 : end - history.size(); if (isForward) { for (int i = start; i < end; i++) { if (history.get(i).toString().contains(searchTerm)) { idx = i; break; } } } else { for (int i = end-1; i >= start; i--) { if (history.get(i).toString().contains(searchTerm)) { idx = i; break; } } } /* * No match? Then restore what we were working on, but make sure * the cursor is at the beginning of the line. */ if (idx == -1) { setCursorPosition(0); killLine(); putString(origBuffer.buffer); setCursorPosition(0); return -1; } /* * Show the match. */ setCursorPosition(0); killLine(); putString(history.get(idx)); setCursorPosition(0); flush(); /* * While searching really only the "n" and "N" keys are interpreted * as movement, any other key is treated as if you are editing the * line with it, so we return it back up to the caller for interpretation. */ isComplete = false; while (!isComplete && (ch = readCharacter()) != -1) { boolean forward = isForward; switch (ch) { case 'p': case 'P': forward = !isForward; // Fallthru case 'n': case 'N': boolean isMatch = false; if (forward) { for (int i = idx+1; !isMatch && i < end; i++) { if (history.get(i).toString().contains(searchTerm)) { idx = i; isMatch = true; } } } else { for (int i = idx - 1; !isMatch && i >= start; i--) { if (history.get(i).toString().contains(searchTerm)) { idx = i; isMatch = true; } } } if (isMatch) { setCursorPosition(0); killLine(); putString(history.get(idx)); setCursorPosition(0); } break; default: isComplete = true; } flush(); } /* * Complete? */ return ch; } public void setParenBlinkTimeout(int timeout) { parenBlinkTimeout = timeout; } private void insertClose(String s) throws IOException { putString(s); int closePosition = buf.cursor; moveCursor(-1); viMatch(); if (in.isNonBlockingEnabled()) { in.peek(parenBlinkTimeout); } setCursorPosition(closePosition); } /** * Implements vi style bracket matching ("%" command). The matching * bracket for the current bracket type that you are sitting on is matched. * The logic works like so: * @return true if it worked, false if the cursor was not on a bracket * character or if there was no matching bracket. * @throws IOException */ private boolean viMatch() throws IOException { int pos = buf.cursor; if (pos == buf.length()) { return false; } int type = getBracketType(buf.buffer.charAt (pos)); int move = (type < 0) ? -1 : 1; int count = 1; if (type == 0) return false; while (count > 0) { pos += move; // Fell off the start or end. if (pos < 0 || pos >= buf.buffer.length ()) { return false; } int curType = getBracketType(buf.buffer.charAt (pos)); if (curType == type) { ++count; } else if (curType == -type) { --count; } } /* * Slight adjustment for delete-to, yank-to, change-to to ensure * that the matching paren is consumed */ if (move > 0 && isInViMoveOperationState()) ++pos; setCursorPosition(pos); return true; } /** * Given a character determines what type of bracket it is (paren, * square, curly, or none). * @param ch The character to check * @return 1 is square, 2 curly, 3 parent, or zero for none. The value * will be negated if it is the closing form of the bracket. */ private int getBracketType (char ch) { switch (ch) { case '[': return 1; case ']': return -1; case '{': return 2; case '}': return -2; case '(': return 3; case ')': return -3; default: return 0; } } private boolean deletePreviousWord() throws IOException { StringBuilder killed = new StringBuilder(); char c; while (isDelimiter((c = buf.current()))) { if (c == 0) { break; } killed.append(c); backspace(); } while (!isDelimiter((c = buf.current()))) { if (c == 0) { break; } killed.append(c); backspace(); } String copy = killed.reverse().toString(); killRing.addBackwards(copy); return true; } private boolean deleteNextWord() throws IOException { StringBuilder killed = new StringBuilder(); char c; while (isDelimiter((c = buf.nextChar()))) { if (c == 0) { break; } killed.append(c); delete(); } while (!isDelimiter((c = buf.nextChar()))) { if (c == 0) { break; } killed.append(c); delete(); } String copy = killed.toString(); killRing.add(copy); return true; } private boolean capitalizeWord() throws IOException { boolean first = true; int i = 1; char c; while (buf.cursor + i - 1< buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { buf.buffer.setCharAt(buf.cursor + i - 1, first ? Character.toUpperCase(c) : Character.toLowerCase(c)); first = false; i++; } drawBuffer(); moveCursor(i - 1); return true; } private boolean upCaseWord() throws IOException { int i = 1; char c; while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { buf.buffer.setCharAt(buf.cursor + i - 1, Character.toUpperCase(c)); i++; } drawBuffer(); moveCursor(i - 1); return true; } private boolean downCaseWord() throws IOException { int i = 1; char c; while (buf.cursor + i - 1 < buf.length() && !isDelimiter((c = buf.buffer.charAt(buf.cursor + i - 1)))) { buf.buffer.setCharAt(buf.cursor + i - 1, Character.toLowerCase(c)); i++; } drawBuffer(); moveCursor(i - 1); return true; } /** * Performs character transpose. The character prior to the cursor and the * character under the cursor are swapped and the cursor is advanced one * character unless you are already at the end of the line. * * @param count The number of times to perform the transpose * @return true if the operation succeeded, false otherwise (e.g. transpose * cannot happen at the beginning of the line). * @throws IOException */ private boolean transposeChars(int count) throws IOException { for (; count > 0; --count) { if (buf.cursor == 0 || buf.cursor == buf.buffer.length()) { return false; } int first = buf.cursor-1; int second = buf.cursor; char tmp = buf.buffer.charAt (first); buf.buffer.setCharAt(first, buf.buffer.charAt(second)); buf.buffer.setCharAt(second, tmp); // This could be done more efficiently by only re-drawing at the end. moveInternal(-1); drawBuffer(); moveInternal(2); } return true; } public boolean isKeyMap(String name) { // Current keymap. KeyMap map = consoleKeys.getKeys(); KeyMap mapByName = consoleKeys.getKeyMaps().get(name); if (mapByName == null) return false; /* * This may not be safe to do, but there doesn't appear to be a * clean way to find this information out. */ return map == mapByName; } /** * The equivalent of hitting <RET>. The line is considered * complete and is returned. * * @return The completed line of text. * @throws IOException */ public String accept() throws IOException { moveToEnd(); println(); // output newline flush(); return finishBuffer(); } private void abort() throws IOException { beep(); buf.clear(); println(); redrawLine(); } /** * Move the cursor where characters. * * @param num If less than 0, move abs(where) to the left, otherwise move where to the right. * @return The number of spaces we moved */ public int moveCursor(final int num) throws IOException { int where = num; if ((buf.cursor == 0) && (where <= 0)) { return 0; } if ((buf.cursor == buf.buffer.length()) && (where >= 0)) { return 0; } if ((buf.cursor + where) < 0) { where = -buf.cursor; } else if ((buf.cursor + where) > buf.buffer.length()) { where = buf.buffer.length() - buf.cursor; } moveInternal(where); return where; } /** * Move the cursor where characters, without checking the current buffer. * * @param where the number of characters to move to the right or left. */ private void moveInternal(final int where) throws IOException { // debug ("move cursor " + where + " (" // + buf.cursor + " => " + (buf.cursor + where) + ")"); buf.cursor += where; if (terminal.isAnsiSupported()) { if (where < 0) { back(Math.abs(where)); } else { int width = getTerminal().getWidth(); int cursor = getCursorPosition(); int oldLine = (cursor - where) / width; int newLine = cursor / width; if (newLine > oldLine) { printAnsiSequence((newLine - oldLine) + "B"); } printAnsiSequence(1 +(cursor % width) + "G"); } // flush(); return; } char c; if (where < 0) { int len = 0; for (int i = buf.cursor; i < buf.cursor - where; i++) { if (buf.buffer.charAt(i) == '\t') { len += TAB_WIDTH; } else { len++; } } char chars[] = new char[len]; Arrays.fill(chars, BACKSPACE); out.write(chars); return; } else if (buf.cursor == 0) { return; } else if (mask != null) { c = mask; } else { print(buf.buffer.substring(buf.cursor - where, buf.cursor).toCharArray()); return; } // null character mask: don't output anything if (mask == NULL_MASK) { return; } print(c, Math.abs(where)); } // FIXME: replace() is not used public final boolean replace(final int num, final String replacement) { buf.buffer.replace(buf.cursor - num, buf.cursor, replacement); try { moveCursor(-num); drawBuffer(Math.max(0, num - replacement.length())); moveCursor(replacement.length()); } catch (IOException e) { e.printStackTrace(); return false; } return true; } /** * Read a character from the console. * * @return the character, or -1 if an EOF is received. */ public final int readCharacter() throws IOException { int c = reader.read(); if (c >= 0) { Log.trace("Keystroke: ", c); // clear any echo characters if (terminal.isSupported()) { clearEcho(c); } } return c; } /** * Clear the echoed characters for the specified character code. */ private int clearEcho(final int c) throws IOException { // if the terminal is not echoing, then ignore if (!terminal.isEchoEnabled()) { return 0; } // otherwise, clear int num = countEchoCharacters(c); back(num); drawBuffer(num); return num; } private int countEchoCharacters(final int c) { // tabs as special: we need to determine the number of spaces // to cancel based on what out current cursor position is if (c == 9) { int tabStop = 8; // will this ever be different? int position = getCursorPosition(); return tabStop - (position % tabStop); } return getPrintableCharacters(c).length(); } /** * Return the number of characters that will be printed when the specified * character is echoed to the screen * * Adapted from cat by Torbjorn Granlund, as repeated in stty by David MacKenzie. */ private StringBuilder getPrintableCharacters(final int ch) { StringBuilder sbuff = new StringBuilder(); if (ch >= 32) { if (ch < 127) { sbuff.append(ch); } else if (ch == 127) { sbuff.append('^'); sbuff.append('?'); } else { sbuff.append('M'); sbuff.append('-'); if (ch >= (128 + 32)) { if (ch < (128 + 127)) { sbuff.append((char) (ch - 128)); } else { sbuff.append('^'); sbuff.append('?'); } } else { sbuff.append('^'); sbuff.append((char) (ch - 128 + 64)); } } } else { sbuff.append('^'); sbuff.append((char) (ch + 64)); } return sbuff; } public final int readCharacter(final char... allowed) throws IOException { // if we restrict to a limited set and the current character is not in the set, then try again. char c; Arrays.sort(allowed); // always need to sort before binarySearch while (Arrays.binarySearch(allowed, c = (char) readCharacter()) < 0) { // nothing } return c; } // // Key Bindings // public static final String JLINE_COMPLETION_THRESHOLD = "jline.completion.threshold"; // // Line Reading // /** * Read the next line and return the contents of the buffer. */ public String readLine() throws IOException { return readLine((String) null); } /** * Read the next line with the specified character mask. If null, then * characters will be echoed. If 0, then no characters will be echoed. */ public String readLine(final Character mask) throws IOException { return readLine(null, mask); } public String readLine(final String prompt) throws IOException { return readLine(prompt, null); } /** * Sets the current keymap by name. Supported keymaps are "emacs", * "vi-insert", "vi-move". * @param name The name of the keymap to switch to * @return true if the keymap was set, or false if the keymap is * not recognized. */ public boolean setKeyMap(String name) { return consoleKeys.setKeyMap(name); } /** * Returns the name of the current key mapping. * @return the name of the key mapping. This will be the canonical name * of the current mode of the key map and may not reflect the name that * was used with {@link #setKeyMap(String)}. */ public String getKeyMap() { return consoleKeys.getKeys().getName(); } /** * Read a line from the in {@link InputStream}, and return the line * (without any trailing newlines). * * @param prompt The prompt to issue to the console, may be null. * @return A line that is read from the terminal, or null if there was null input (e.g., CTRL-D * was pressed). */ public String readLine(String prompt, final Character mask) throws IOException { // prompt may be null // mask may be null /* * This is the accumulator for VI-mode repeat count. That is, while in * move mode, if you type 30x it will delete 30 characters. This is * where the "30" is accumulated until the command is struck. */ int repeatCount = 0; // FIXME: This blows, each call to readLine will reset the console's state which doesn't seem very nice. this.mask = mask; if (prompt != null) { setPrompt(prompt); } else { prompt = getPrompt(); } try { if (!terminal.isSupported()) { beforeReadLine(prompt, mask); } if (prompt != null && prompt.length() > 0) { out.write(prompt); out.flush(); } // if the terminal is unsupported, just use plain-java reading if (!terminal.isSupported()) { return readLineSimple(); } if (handleUserInterrupt && (terminal instanceof UnixTerminal)) { ((UnixTerminal) terminal).disableInterruptCharacter(); } String originalPrompt = this.prompt; state = State.NORMAL; boolean success = true; StringBuilder sb = new StringBuilder(); Stack