/*
                                 * This is a weird special case. In vi
                                 * "dd" deletes the current line. So if we
                                 * get a delete-to, followed by a delete-to,
                                 * we delete the line.
                                 */
public String readLine(String prompt, final Character mask, String buffer) throws IOException {
        // prompt may be null
        // mask may be null
        // buffer 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 != null ? mask : this.echoCharacter;
        if (prompt != null) {
            setPrompt(prompt);
        }
        else {
            prompt = getPrompt();
        }
        try {
            if (buffer != null) {
                buf.write(buffer);
            }
            if (!terminal.isSupported()) {
                beforeReadLine(prompt, mask);
            }
            if (buffer != null && buffer.length() > 0
                    || prompt != null && prompt.length() > 0) {
                drawLine();
                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();
            }
            if (handleLitteralNext && (terminal instanceof UnixTerminal)) {
                ((UnixTerminal) terminal).disableLitteralNextCharacter();
            }
            String originalPrompt = this.prompt;
            state = State.NORMAL;
            boolean success = true;
            pushBackChar.clear();
            while (true) {
                Object o = readBinding(getKeys());
                if (o == null) {
                    return null;
                }
                int c = 0;
                if (opBuffer.length() > 0) {
                    c = opBuffer.codePointBefore(opBuffer.length());
                }
                Log.trace("Binding: ", o);
                // Handle macros
                if (o instanceof String) {
                    String macro = (String) o;
                    for (int i = 0; i < macro.length(); i++) {
                        pushBackChar.push(macro.charAt(macro.length() - 1 - i));
                    }
                    opBuffer.setLength(0);
                    continue;
                }
                // Handle custom callbacks
                if (o instanceof ActionListener) {
                    ((ActionListener) o).actionPerformed(null);
                    opBuffer.setLength(0);
                    continue;
                }
                CursorBuffer oldBuf = new CursorBuffer();
                oldBuf.buffer.append(buf.buffer);
                oldBuf.cursor = buf.cursor;
                // Search mode.
                //
                // Note that we have to do this first, because if there is a command
                // not linked to a search command, we leave the search mode and fall
                // through to the normal state.
                if (state == State.SEARCH || state == State.FORWARD_SEARCH) {
                    int cursorDest = -1;
                    // TODO: check the isearch-terminators variable terminating the search
                    switch ( ((Operation) o )) {
                        case ABORT:
                            state = State.NORMAL;
                            buf.clear();
                            buf.write(originalBuffer.buffer);
                            buf.cursor = originalBuffer.cursor;
                            break;
                        case REVERSE_SEARCH_HISTORY:
                            state = State.SEARCH;
                            if (searchTerm.length() == 0) {
                                searchTerm.append(previousSearchTerm);
                            }
                            if (searchIndex > 0) {
                                searchIndex = searchBackwards(searchTerm.toString(), searchIndex);
                            }
                            break;
                        case FORWARD_SEARCH_HISTORY:
                            state = State.FORWARD_SEARCH;
                            if (searchTerm.length() == 0) {
                                searchTerm.append(previousSearchTerm);
                            }
                            if (searchIndex > -1 && searchIndex < history.size() - 1) {
                                searchIndex = searchForwards(searchTerm.toString(), searchIndex);
                            }
                            break;
                        case BACKWARD_DELETE_CHAR:
                            if (searchTerm.length() > 0) {
                                searchTerm.deleteCharAt(searchTerm.length() - 1);
                                if (state == State.SEARCH) {
                                    searchIndex = searchBackwards(searchTerm.toString());
                                } else {
                                    searchIndex = searchForwards(searchTerm.toString());
                                }
                            }
                            break;
                        case SELF_INSERT:
                            searchTerm.appendCodePoint(c);
                            if (state == State.SEARCH) {
                                searchIndex = searchBackwards(searchTerm.toString());
                            } else {
                                searchIndex = searchForwards(searchTerm.toString());
                            }
                            break;
                        default:
                            // Set buffer and cursor position to the found string.
                            if (searchIndex != -1) {
                                history.moveTo(searchIndex);
                                // set cursor position to the found string
                                cursorDest = history.current().toString().indexOf(searchTerm.toString());
                            }
                            if (o != Operation.ACCEPT_LINE) {
                                o = null;
                            }
                            state = State.NORMAL;
                            break;
                    }
                    // if we're still in search mode, print the search status
                    if (state == State.SEARCH || state == State.FORWARD_SEARCH) {
                        if (searchTerm.length() == 0) {
                            if (state == State.SEARCH) {
                                printSearchStatus("", "");
                            } else {
                                printForwardSearchStatus("", "");
                            }
                            searchIndex = -1;
                        } else {
                            if (searchIndex == -1) {
                                beep();
                                printSearchStatus(searchTerm.toString(), "");
                            } else if (state == State.SEARCH) {
                                printSearchStatus(searchTerm.toString(), history.get(searchIndex).toString());
                            } else {
                                printForwardSearchStatus(searchTerm.toString(), history.get(searchIndex).toString());
                            }
                        }
                    }
                    // otherwise, restore the line
                    else {
                        restoreLine(originalPrompt, cursorDest);
                    }
                }
                if (state != State.SEARCH && state != State.FORWARD_SEARCH) {
                    /*
                     * If this is still false at the end of the switch, then
                     * we reset our repeatCount to 0.
                     */
                    boolean isArgDigit = false;
                    /*
                     * Every command that can be repeated a specified number
                     * of times, needs to know how many times to repeat, so
                     * we figure that out here.
                     */
                    int count = (repeatCount == 0) ? 1 : repeatCount;
                    /*
                     * Default success to true. You only need to explicitly
                     * set it if something goes wrong.
                     */
                    success = true;
                    if (o instanceof Operation) {
                        Operation op = (Operation)o;
                        /*
                         * Current location of the cursor (prior to the operation).
                         * These are used by vi *-to operation (e.g. delete-to)
                         * so we know where we came from.
                         */
                        int     cursorStart = buf.cursor;
                        State   origState   = state;
                        /*
                         * If we are on a "vi" movement based operation, then we
                         * need to restrict the sets of inputs pretty heavily.
                         */
                        if (state == State.VI_CHANGE_TO
                            || state == State.VI_YANK_TO
                            || state == State.VI_DELETE_TO) {
                            op = viDeleteChangeYankToRemap(op);
                        }
                        switch ( op ) {
                            case COMPLETE: // tab
                                // There is an annoyance with tab completion in that
                                // sometimes the user is actually pasting input in that
                                // has physical tabs in it.  This attempts to look at how
                                // quickly a character follows the tab, if the character
                                // follows *immediately*, we assume it is a tab literal.
                                boolean isTabLiteral = false;
                                if (copyPasteDetection
                                    && c == 9
                                    && (!pushBackChar.isEmpty()
                                        || (in.isNonBlockingEnabled() && in.peek(escapeTimeout) != -2))) {
                                    isTabLiteral = true;
                                }
                                if (! isTabLiteral) {
                                    success = complete();
                                }
                                else {
                                    putString(opBuffer);
                                }
                                break;
                            case POSSIBLE_COMPLETIONS:
                                printCompletionCandidates();
                                break;
                            case BEGINNING_OF_LINE:
                                success = setCursorPosition(0);
                                break;
                            case YANK:
                                success = yank();
                                break;
                            case YANK_POP:
                                success = yankPop();
                                break;
                            case KILL_LINE: // CTRL-K
                                success = killLine();
                                break;
                            case KILL_WHOLE_LINE:
                                success = setCursorPosition(0) && killLine();
                                break;
                            case CLEAR_SCREEN: // CTRL-L
                                success = clearScreen();
                                redrawLine();
                                break;
                            case OVERWRITE_MODE:
                                buf.setOverTyping(!buf.isOverTyping());
                                break;
                            case SELF_INSERT:
                                putString(opBuffer);
                                break;
                            case ACCEPT_LINE:
                                return accept();
                            case ABORT:
                                if (searchTerm == null) {
                                    abort();
                                }
                                break;
                            case INTERRUPT:
                                if (handleUserInterrupt) {
                                    println();
                                    flush();
                                    String partialLine = buf.buffer.toString();
                                    buf.clear();
                                    history.moveToEnd();
                                    throw new UserInterruptException(partialLine);
                                }
                                break;
                            /*
                             * VI_MOVE_ACCEPT_LINE is the result of an ENTER
                             * while in move mode. This is the same as a normal
                             * ACCEPT_LINE, except that we need to enter
                             * insert mode as well.
                             */
                            case VI_MOVE_ACCEPT_LINE:
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                return accept();
                            case BACKWARD_WORD:
                                success = previousWord();
                                break;
                            case FORWARD_WORD:
                                success = nextWord();
                                break;
                            case PREVIOUS_HISTORY:
                                success = moveHistory(false);
                                break;
                            /*
                             * According to bash/readline move through history
                             * in "vi" mode will move the cursor to the
                             * start of the line. If there is no previous
                             * history, then the cursor doesn't move.
                             */
                            case VI_PREVIOUS_HISTORY:
                                success = moveHistory(false, count)
                                    && setCursorPosition(0);
                                break;
                            case NEXT_HISTORY:
                                success = moveHistory(true);
                                break;
                            /*
                             * According to bash/readline move through history
                             * in "vi" mode will move the cursor to the
                             * start of the line. If there is no next history,
                             * then the cursor doesn't move.
                             */
                            case VI_NEXT_HISTORY:
                                success = moveHistory(true, count)
                                    && setCursorPosition(0);
                                break;
                            case BACKWARD_DELETE_CHAR: // backspace
                                success = backspace();
                                break;
                            case EXIT_OR_DELETE_CHAR:
                                if (buf.buffer.length() == 0) {
                                    return null;
                                }
                                success = deleteCurrentCharacter();
                                break;
                            case DELETE_CHAR: // delete
                                success = deleteCurrentCharacter();
                                break;
                            case BACKWARD_CHAR:
                                success = moveCursor(-(count)) != 0;
                                break;
                            case FORWARD_CHAR:
                                success = moveCursor(count) != 0;
                                break;
                            case UNIX_LINE_DISCARD:
                                success = resetLine();
                                break;
                            case UNIX_WORD_RUBOUT:
                                success = unixWordRubout(count);
                                break;
                            case BACKWARD_KILL_WORD:
                                success = deletePreviousWord();
                                break;
                            case KILL_WORD:
                                success = deleteNextWord();
                                break;
                            case BEGINNING_OF_HISTORY:
                                success = history.moveToFirst();
                                if (success) {
                                    setBuffer(history.current());
                                }
                                break;
                            case END_OF_HISTORY:
                                success = history.moveToLast();
                                if (success) {
                                    setBuffer(history.current());
                                }
                                break;
                            case HISTORY_SEARCH_BACKWARD:
                                searchTerm = new StringBuffer(buf.upToCursor());
                                searchIndex = searchBackwards(searchTerm.toString(), history.index(), true);
                                if (searchIndex == -1) {
                                    beep();
                                } else {
                                    // Maintain cursor position while searching.
                                    success = history.moveTo(searchIndex);
                                    if (success) {
                                        setBufferKeepPos(history.current());
                                    }
                                }
                                break;
                            case HISTORY_SEARCH_FORWARD:
                                searchTerm = new StringBuffer(buf.upToCursor());
                                int index = history.index() + 1;
                                if (index == history.size()) {
                                    history.moveToEnd();
                                    setBufferKeepPos(searchTerm.toString());
                                } else if (index < history.size()) {
                                    searchIndex = searchForwards(searchTerm.toString(), index, true);
                                    if (searchIndex == -1) {
                                        beep();
                                    } else {
                                        // Maintain cursor position while searching.
                                        success = history.moveTo(searchIndex);
                                        if (success) {
                                            setBufferKeepPos(history.current());
                                        }
                                    }
                                }
                                break;
                            case REVERSE_SEARCH_HISTORY:
                                originalBuffer = new CursorBuffer();
                                originalBuffer.write(buf.buffer);
                                originalBuffer.cursor = buf.cursor;
                                if (searchTerm != null) {
                                    previousSearchTerm = searchTerm.toString();
                                }
                                searchTerm = new StringBuffer(buf.buffer);
                                state = State.SEARCH;
                                if (searchTerm.length() > 0) {
                                    searchIndex = searchBackwards(searchTerm.toString());
                                    if (searchIndex == -1) {
                                        beep();
                                    }
                                    printSearchStatus(searchTerm.toString(),
                                            searchIndex > -1 ? history.get(searchIndex).toString() : "");
                                } else {
                                    searchIndex = -1;
                                    printSearchStatus("", "");
                                }
                                break;
                            case FORWARD_SEARCH_HISTORY:
                                originalBuffer = new CursorBuffer();
                                originalBuffer.write(buf.buffer);
                                originalBuffer.cursor = buf.cursor;
                                if (searchTerm != null) {
                                    previousSearchTerm = searchTerm.toString();
                                }
                                searchTerm = new StringBuffer(buf.buffer);
                                state = State.FORWARD_SEARCH;
                                if (searchTerm.length() > 0) {
                                    searchIndex = searchForwards(searchTerm.toString());
                                    if (searchIndex == -1) {
                                        beep();
                                    }
                                    printForwardSearchStatus(searchTerm.toString(),
                                            searchIndex > -1 ? history.get(searchIndex).toString() : "");
                                } else {
                                    searchIndex = -1;
                                    printForwardSearchStatus("", "");
                                }
                                break;
                            case CAPITALIZE_WORD:
                                success = capitalizeWord();
                                break;
                            case UPCASE_WORD:
                                success = upCaseWord();
                                break;
                            case DOWNCASE_WORD:
                                success = downCaseWord();
                                break;
                            case END_OF_LINE:
                                success = moveToEnd();
                                break;
                            case TAB_INSERT:
                                putString( "\t" );
                                break;
                            case RE_READ_INIT_FILE:
                                consoleKeys.loadKeys(appName, inputrcUrl);
                                break;
                            case START_KBD_MACRO:
                                recording = true;
                                break;
                            case END_KBD_MACRO:
                                recording = false;
                                macro = macro.substring(0, macro.length() - opBuffer.length());
                                break;
                            case CALL_LAST_KBD_MACRO:
                                for (int i = 0; i < macro.length(); i++) {
                                    pushBackChar.push(macro.charAt(macro.length() - 1 - i));
                                }
                                opBuffer.setLength(0);
                                break;
                            case VI_EDITING_MODE:
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                break;
                            case VI_MOVEMENT_MODE:
                                /*
                                 * If we are re-entering move mode from an
                                 * aborted yank-to, delete-to, change-to then
                                 * don't move the cursor back. The cursor is
                                 * only move on an expclit entry to movement
                                 * mode.
                                 */
                                if (state == State.NORMAL) {
                                    moveCursor(-1);
                                }
                                consoleKeys.setKeyMap(KeyMap.VI_MOVE);
                                break;
                            case VI_INSERTION_MODE:
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                break;
                            case VI_APPEND_MODE:
                                moveCursor(1);
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                break;
                            case VI_APPEND_EOL:
                                success = moveToEnd();
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                break;
                            /*
                             * Handler for CTRL-D. Attempts to follow readline
                             * behavior. If the line is empty, then it is an EOF
                             * otherwise it is as if the user hit enter.
                             */
                            case VI_EOF_MAYBE:
                                if (buf.buffer.length() == 0) {
                                    return null;
                                }
                                return accept();
                            case TRANSPOSE_CHARS:
                                success = transposeChars(count);
                                break;
                            case INSERT_COMMENT:
                                return insertComment (false);
                            case INSERT_CLOSE_CURLY:
                                insertClose("}");
                                break;
                            case INSERT_CLOSE_PAREN:
                                insertClose(")");
                                break;
                            case INSERT_CLOSE_SQUARE:
                                insertClose("]");
                                break;
                            case VI_INSERT_COMMENT:
                                return insertComment (true);
                            case VI_MATCH:
                                success = viMatch ();
                                break;
                            case VI_SEARCH:
                                int lastChar = viSearch(opBuffer.charAt(0));
                                if (lastChar != -1) {
                                    pushBackChar.push((char)lastChar);
                                }
                                break;
                            case VI_ARG_DIGIT:
                                repeatCount = (repeatCount * 10) + opBuffer.charAt(0) - '0';
                                isArgDigit = true;
                                break;
                            case VI_BEGINNING_OF_LINE_OR_ARG_DIGIT:
                                if (repeatCount > 0) {
                                    repeatCount = (repeatCount * 10) + opBuffer.charAt(0) - '0';
                                    isArgDigit = true;
                                }
                                else {
                                    success = setCursorPosition(0);
                                }
                                break;
                            case VI_FIRST_PRINT:
                                success = setCursorPosition(0) && viNextWord(1);
                                break;
                            case VI_PREV_WORD:
                                success = viPreviousWord(count);
                                break;
                            case VI_NEXT_WORD:
                                success = viNextWord(count);
                                break;
                            case VI_END_WORD:
                                success = viEndWord(count);
                                break;
                            case VI_INSERT_BEG:
                                success = setCursorPosition(0);
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                break;
                            case VI_RUBOUT:
                                success = viRubout(count);
                                break;
                            case VI_DELETE:
                                success = viDelete(count);
                                break;
                            case VI_DELETE_TO:
                                /*
                                 * This is a weird special case. In vi
                                 * "dd" deletes the current line. So if we
                                 * get a delete-to, followed by a delete-to,
                                 * we delete the line.
                                 */
                                if (state == State.VI_DELETE_TO) {
                                    success = setCursorPosition(0) && killLine();
                                    state = origState = State.NORMAL;
                                }
                                else {
                                    state = State.VI_DELETE_TO;
                                }
                                break;
                            case VI_YANK_TO:
                                // Similar to delete-to, a "yy" yanks the whole line.
                                if (state == State.VI_YANK_TO) {
                                    yankBuffer = buf.buffer.toString();
                                    state = origState = State.NORMAL;
                                }
                                else {
                                    state = State.VI_YANK_TO;
                                }
                                break;
                            case VI_CHANGE_TO:
                                if (state == State.VI_CHANGE_TO) {
                                    success = setCursorPosition(0) && killLine();
                                    state = origState = State.NORMAL;
                                    consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                }
                                else {
                                    state = State.VI_CHANGE_TO;
                                }
                                break;
                            case VI_KILL_WHOLE_LINE:
                                success = setCursorPosition(0) && killLine();
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                break;
                            case VI_PUT:
                                success = viPut(count);
                                break;
                            case VI_CHAR_SEARCH: {
                                 // ';' and ',' don't need another character. They indicate repeat next or repeat prev.
                                int searchChar = (c != ';' && c != ',')
                                    ? (pushBackChar.isEmpty()
                                        ? readCharacter()
                                        : pushBackChar.pop ())
                                    : 0;
                                    success = viCharSearch(count, c, searchChar);
                                }
                                break;
                            case VI_CHANGE_CASE:
                                success = viChangeCase(count);
                                break;
                            case VI_CHANGE_CHAR:
                                success = viChangeChar(count,
                                    pushBackChar.isEmpty()
                                        ? readCharacter()
                                        : pushBackChar.pop());
                                break;
                            case VI_DELETE_TO_EOL:
                                success = viDeleteTo(buf.cursor, buf.buffer.length(), false);
                                break;
                            case VI_CHANGE_TO_EOL:
                                success = viDeleteTo(buf.cursor, buf.buffer.length(), true);
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                                break;
                            case EMACS_EDITING_MODE:
                                consoleKeys.setKeyMap(KeyMap.EMACS);
                                break;
                            case QUIT:
                                getCursorBuffer().clear();
                                return accept();
                            case QUOTED_INSERT:
                                quotedInsert = true;
                                break;
                            case PASTE_FROM_CLIPBOARD:
                                paste();
                                break;
                            default:
                                break;
                        }
                        /*
                         * If we were in a yank-to, delete-to, move-to
                         * when this operation started, then fall back to
                         */
                        if (origState != State.NORMAL) {
                            if (origState == State.VI_DELETE_TO) {
                                success = viDeleteTo(cursorStart, buf.cursor, false);
                            }
                            else if (origState == State.VI_CHANGE_TO) {
                                success = viDeleteTo(cursorStart, buf.cursor, true);
                                consoleKeys.setKeyMap(KeyMap.VI_INSERT);
                            }
                            else if (origState == State.VI_YANK_TO) {
                                success = viYankTo(cursorStart, buf.cursor);
                            }
                            state = State.NORMAL;
                        }
                        /*
                         * Another subtly. The check for the NORMAL state is
                         * to ensure that we do not clear out the repeat
                         * count when in delete-to, yank-to, or move-to modes.
                         */
                        if (state == State.NORMAL && !isArgDigit) {
                            /*
                             * If the operation performed wasn't a vi argument
                             * digit, then clear out the current repeatCount;
                             */
                            repeatCount = 0;
                        }
                        if (state != State.SEARCH && state != State.FORWARD_SEARCH) {
                            originalBuffer = null;
                            previousSearchTerm = "";
                            searchTerm = null;
                            searchIndex = -1;
                        }
                    }
                }
                if (!success) {
                    beep();
                }
                opBuffer.setLength(0);
                flush();
            }
        }
        finally {
            if (!terminal.isSupported()) {
                afterReadLine();
            }
            if (handleUserInterrupt && (terminal instanceof UnixTerminal)) {
                ((UnixTerminal) terminal).enableInterruptCharacter();
            }
        }
    }
// mask may be null
// buffer 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 != null ? mask : this.echoCharacter;
public String readLine(String prompt, final Character mask, String buffer) throws IOException {
// prompt may be null
// mask may be null
// buffer 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 != null ? mask : this.echoCharacter;
if (prompt != null) {
setPrompt(prompt);
}
else {
prompt = getPrompt();
}
try {
if (buffer != null) {
public String readLine(String prompt, final Character mask, String buffer) throws IOException {
// prompt may be null
// mask may be null
// buffer 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 != null ? mask : this.echoCharacter;
if (prompt != null) {
setPrompt(prompt);
}
else {
prompt = getPrompt();
}
try {
if (buffer != null) {
buf.write(buffer);
}
if (!terminal.isSupported()) {
beforeReadLine(prompt, mask);
}
if (buffer != null && buffer.length() > 0
|| prompt != null && prompt.length() > 0) {
drawLine();
out.flush();
}