/*
 * Copyright (c) 2002-2016, 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 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.Closeable;
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.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.System;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.ResourceBundle;
import java.util.Stack;

import jline.DefaultTerminal2;
import jline.Terminal;
import jline.Terminal2;
import jline.TerminalFactory;
import jline.UnixTerminal;
import jline.console.completer.CandidateListCompletionHandler;
import jline.console.completer.Completer;
import jline.console.completer.CompletionHandler;
import jline.console.history.History;
import jline.console.history.MemoryHistory;
import jline.internal.Ansi;
import jline.internal.Configuration;
import jline.internal.Curses;
import jline.internal.InputStreamReader;
import jline.internal.Log;
import jline.internal.NonBlockingInputStream;
import jline.internal.Nullable;
import jline.internal.TerminalLineSettings;
import jline.internal.Urls;

import static 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 <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
 */
public class ConsoleReader implements Closeable
{
    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 String JLINE_EXPAND_EVENTS = "jline.expandevents";

    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 = 8;

    private static final ResourceBundle
        resources = ResourceBundle.getBundle(CandidateListCompletionHandler.class.getName());

    private static final int ESCAPE = 27;
    private static final int READ_EXPIRED = -2;

    private final Terminal2 terminal;

    private final Writer out;

    private final CursorBuffer buf = new CursorBuffer();
    private boolean cursorOk;

    private String prompt;
    private int    promptLen;

    private boolean expandEvents = Configuration.getBoolean(JLINE_EXPAND_EVENTS, true);

    private boolean bellEnabled = !Configuration.getBoolean(JLINE_NOBELL, true);

    private boolean handleUserInterrupt = false;

    private boolean handleLitteralNext = true;

    private Character mask;

    private Character echoCharacter;

    private CursorBuffer originalBuffer = null;

    private StringBuffer searchTerm = null;

    private String previousSearchTerm = "";

    private int searchIndex = -1;

    private int parenBlinkTimeout = 500;

    // Reading buffers
    private final StringBuilder opBuffer = new StringBuilder();
    private final Stack<Character> pushBackChar = new Stack<Character>();

    /*
     * 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;

    /**
     * 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 quotedInsert;

    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();
        Terminal terminal = term != null ? term : TerminalFactory.get();
        this.terminal = terminal instanceof Terminal2 ? (Terminal2) terminal : new DefaultTerminal2(terminal);
        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);

        if (terminal instanceof UnixTerminal
                && TerminalLineSettings.DEFAULT_TTY.equals(((UnixTerminal) terminal).getSettings().getTtyDevice())
                && Configuration.getBoolean("jline.sigcont", false)) {
            setupSigCont();
        }
    }

    private void setupSigCont() {
        // Check that sun.misc.SignalHandler and sun.misc.Signal exists
        try {
            Class<?> signalClass = Class.forName("sun.misc.Signal");
            Class<?> signalHandlerClass = Class.forName("sun.misc.SignalHandler");
            // Implement signal handler
            Object signalHandler = Proxy.newProxyInstance(getClass().getClassLoader(),
                    new Class<?>[]{signalHandlerClass}, new InvocationHandler() {
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            // only method we are proxying is handle()
                            terminal.init();
                            try {
                                drawLine();
                                flush();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                            return null;
                        }
                    });
            // Register the signal handler, this code is equivalent to:
            // Signal.handle(new Signal("CONT"), signalHandler);
            signalClass.getMethod("handle", signalClass, signalHandlerClass).invoke(null, signalClass.getConstructor(String.class).newInstance("CONT"), signalHandler);
        } catch (ClassNotFoundException cnfe) {
            // sun.misc Signal handler classes don't exist
        } catch (Exception e) {
            // Ignore this one too, if the above failed, the signal API is incompatible with what we're expecting
        }
    }

    /**
     * Retrieve the URL for the inputrc configuration file in effect. Intended
     * use is for instantiating ConsoleKeys, to read inputrc variables.
     */
    public static 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);
        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".
     */
    @Override
    public void close() {
        if (in != null) {
            in.shutdown();
        }
    }

    /**
     * Shuts the console reader down.  The same as {@link #close()}.
     * @deprecated Use {@link #close()} instead.
     */
    @Deprecated
    public void shutdown() {
        this.close();
    }
    
    /**
     * Shuts down the ConsoleReader if the JVM attempts to clean it up.
     */
    @Override
    protected void finalize() throws Throwable {
        try {
            close();
        }
        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;
    }

    /**
     * Set wether literal next are handled by JLine.
     *
     * @since 2.13
     */
    public void setHandleLitteralNext(boolean handleLitteralNext) {
        this.handleLitteralNext = handleLitteralNext;
    }

    /**
     * Get wether literal next are handled by JLine.
     *
     * @since 2.13
     */
    public boolean getHandleLitteralNext() {
        return handleLitteralNext;
    }

    /**
     * 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 : wcwidth(Ansi.stripAnsi(lastLine(prompt)), 0);
    }

    public String getPrompt() {
        return prompt;
    }

    /**
     * Set the echo character. For example, to have "*" entered when a password is typed:
     * <pre>
     * myConsoleReader.setEchoCharacter(new Character('*'));
     * </pre>
     * Setting the character to <code>null</code> will restore normal character echoing.<p/>
     * Setting the character to <code>Character.valueOf(0)</code> 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 wcwidth(CharSequence str, int pos) {
        return wcwidth(str, 0, str.length(), pos);
    }

    int wcwidth(CharSequence str, int start, int end, int pos) {
        int cur = pos;
        for (int i = start; i < end;) {
            int ucs;
            char c1 = str.charAt(i++);
            if (!Character.isHighSurrogate(c1) || i >= end) {
                ucs = c1;
            } else {
                char c2 = str.charAt(i);
                if (Character.isLowSurrogate(c2)) {
                    i++;
                    ucs = Character.toCodePoint(c1, c2);
                } else {
                    ucs = c1;
                }
            }
            cur += wcwidth(ucs, cur);
        }
        return cur - pos;
    }

    int wcwidth(int ucs, int pos) {
        if (ucs == '\t') {
            return nextTabStop(pos);
        } else if (ucs < 32) {
            return 2;
        } else  {
            int w = WCWidth.wcwidth(ucs);
            return w > 0 ? w : 0;
        }
    }

    int nextTabStop(int pos) {
        int tabWidth = TAB_WIDTH;
        int width = getTerminal().getWidth();
        int mod = (pos + tabWidth - 1) % tabWidth;
        int npos = pos + tabWidth - mod;
        return npos < width ? npos - pos : width - pos;
    }

    int getCursorPosition() {
        return promptLen + wcwidth(buf.buffer, 0, buf.cursor, promptLen);
    }

    /**
     * Returns the text after the last '\n'.
     * prompt is returned if no '\n' characters are present.
     * null is returned if prompt is null.
     */
    private static 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;
    }

    /**
     * Move the cursor position to the specified absolute index.
     */
    public 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 void drawLine() throws IOException {
        String prompt = getPrompt();
        if (prompt != null) {
            rawPrint(prompt);
        }

        fmtPrint(buf.buffer, 0, buf.cursor, promptLen);

        // force drawBuffer to check for weird wrap (after clear screen)
        drawBuffer();
    }

    /**
     * Clear the line and redraw it.
     */
    public void redrawLine() throws IOException {
        tputs("carriage_return");
        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)) {
            fmtPrint(result, getCursorPosition());
            println();
            flush();
        }
        return result;

    }

    /**
     * Write out the specified string to the buffer and the output stream.
     */
    public void putString(final CharSequence str) throws IOException {
        int pos = getCursorPosition();
        buf.write(str);
        if (mask == null) {
            // no masking
            fmtPrint(str, pos);
        } else if (mask == NULL_MASK) {
            // don't print anything
        } else {
            rawPrint(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);
        int nbChars = buf.length() - buf.cursor;
        if (buf.cursor != buf.length() || clear != 0) {
            if (mask != null) {
                if (mask != NULL_MASK) {
                    rawPrint(mask, nbChars);
                } else {
                    nbChars = 0;
                }
            } else {
                fmtPrint(buf.buffer, buf.cursor, buf.length());
            }
        }
        int cursorPos = promptLen + wcwidth(buf.buffer, 0, buf.length(), promptLen);
        if (terminal.hasWeirdWrap() && !cursorOk) {
            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 (cursorPos > 0 && (cursorPos % width == 0)) {
                // the following workaround is reverse-engineered from looking
                // at what bash sent to the terminal in the same situation
                rawPrint(' '); // move cursor to next line by printing dummy space
                tputs("carriage_return"); // CR / not newline.
            }
            cursorOk = true;
        }
        clearAhead(clear, cursorPos);
        back(nbChars);
    }

    /**
     * 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 pos the current screen cursor position
     */
    private void clearAhead(int num, final int pos) throws IOException {
        if (num == 0) return;

        int width = terminal.getWidth();
        // Use kill line
        if (terminal.getStringCapability("clr_eol") != null) {
            int cur = pos;
            int c0 = cur % width;
            // Erase end of current line
            int nb = Math.min(num, width - c0);
            tputs("clr_eol");
            num -= nb;
            // Loop
            while (num > 0) {
                // Move to beginning of next line
                int prev = cur;
                cur = cur - cur % width + width;
                moveCursorFromTo(prev, cur);
                // Erase
                nb = Math.min(num, width);
                tputs("clr_eol");
                num -= nb;
            }
            moveCursorFromTo(cur, pos);
        }
        // Terminal does not wrap on the right margin
        else if (!terminal.getBooleanCapability("auto_right_margin")) {
            int cur = pos;
            int c0 = cur % width;
            // Erase end of current line
            int nb = Math.min(num, width - c0);
            rawPrint(' ', nb);
            num -= nb;
            cur += nb;
            // Loop
            while (num > 0) {
                // Move to beginning of next line
                moveCursorFromTo(cur, ++cur);
                // Erase
                nb = Math.min(num, width);
                rawPrint(' ', nb);
                num -= nb;
                cur += nb;
            }
            moveCursorFromTo(cur, pos);
        }
        // Simple erasure
        else {
            rawPrint(' ', num);
            moveCursorFromTo(pos + num, pos);
        }
    }

    /**
     * Move the visual cursor backward without modifying the buffer cursor.
     */
    protected void back(final int num) throws IOException {
        if (num == 0) return;
        int i0 = promptLen + wcwidth(buf.buffer, 0, buf.cursor, promptLen);
        int i1 = i0 + ((mask != null) ? num : wcwidth(buf.buffer, buf.cursor, buf.cursor + num, i0));
        moveCursorFromTo(i1, i0);
    }

    /**
     * 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 <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 = - moveCursor(-num);
        int clear = wcwidth(buf.buffer, buf.cursor, buf.cursor + count, getCursorPosition());
        buf.buffer.delete(buf.cursor, buf.cursor + count);

        drawBuffer(clear);
        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_BEGINNING_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.
     */
    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
     */
    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.
     */
    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
     */
    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
     */
    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
     */
    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
     */
    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
     */
    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
     */
    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 static 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
     */
    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.
     */
    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
     */
    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();
    }

    /**
     * Implements vi search ("/" or "?").
     */
    @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);
        flush();
    }

    /**
     * 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.
     */
    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);
        flush();
        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 static 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).
     */
    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 &lt;RET&gt;.  The line is considered
     * complete and is returned.
     *
     * @return The completed line of text.
     */
    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 <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;

        int i0;
        int i1;
        if (mask == null) {
            if (where < 0) {
                i1 = promptLen + wcwidth(buf.buffer, 0, buf.cursor, promptLen);
                i0 = i1 + wcwidth(buf.buffer, buf.cursor, buf.cursor - where, i1);
            } else {
                i0 = promptLen + wcwidth(buf.buffer, 0, buf.cursor - where, promptLen);
                i1 = i0 + wcwidth(buf.buffer, buf.cursor - where, buf.cursor, i0);
            }
        } else if (mask != NULL_MASK) {
            i1 = promptLen + buf.cursor;
            i0 = i1 - where;
        } else {
            return;
        }
        moveCursorFromTo(i0, i1);
    }

    private void moveCursorFromTo(int i0, int i1) throws IOException {
        if (i0 == i1) return;
        int width = getTerminal().getWidth();
        int l0 = i0 / width;
        int c0 = i0 % width;
        int l1 = i1 / width;
        int c1 = i1 % width;
        if (l0 == l1 + 1) {
            if (!tputs("cursor_up")) {
                tputs("parm_up_cursor", 1);
            }
        } else if (l0 > l1) {
            if (!tputs("parm_up_cursor", l0 - l1)) {
                for (int i = l1; i < l0; i++) {
                    tputs("cursor_up");
                }
            }
        } else if (l0 < l1) {
            tputs("carriage_return");
            rawPrint('\n', l1 - l0);
            c0 = 0;
        }
        if (c0 == c1 - 1) {
            tputs("cursor_right");
        } else if (c0 == c1 + 1) {
            tputs("cursor_left");
        } else if (c0 < c1) {
            if (!tputs("parm_right_cursor", c1 - c0)) {
                for (int i = c0; i < c1; i++) {
                    tputs("cursor_right");
                }
            }
        } else if (c0 > c1) {
            if (!tputs("parm_left_cursor", c0 - c1)) {
                for (int i = c1; i < c0; i++) {
                    tputs("cursor_left");
                }
            }
        }
        cursorOk = true;
    }

    /**
     * Read a character from the console.
     *
     * @return the character, or -1 if an EOF is received.
     */
    public int readCharacter() throws IOException {
      return readCharacter(false);
    }

    /**
     * Read a character from the console.  If boolean parameter is "true", it will check whether the keystroke was an "alt-" key combination, and
     * if so add 1000 to the value returned.  Better way...?
     *
     * @return the character, or -1 if an EOF is received.
     */
    public int readCharacter(boolean checkForAltKeyCombo) throws IOException {
        int c = reader.read();
        if (c >= 0) {
            Log.trace("Keystroke: ", c);
            // clear any echo characters
            if (terminal.isSupported()) {
                clearEcho(c);
            }
            if (c == ESCAPE && checkForAltKeyCombo && in.peek(escapeTimeout) >= 32) {
              /* When ESC is encountered and there is a pending
               * character in the pushback queue, then it seems to be
               * an Alt-[key] combination.  Is this true, cross-platform?
               * It's working for me on Debian GNU/Linux at the moment anyway.
               * I removed the "isNonBlockingEnabled" check, though it was
               * in the similar code in "readLine(String prompt, final Character mask)" (way down),
               * as I am not sure / didn't look up what it's about, and things are working so far w/o it.
               */
              int next = reader.read();
              // with research, there's probably a much cleaner way to do this, but, this is now it flags an Alt key combination for now:
              next = next + 1000;
              return next;
            }
        }
        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 pos = getCursorPosition();
        int num = wcwidth(c, pos);
        moveCursorFromTo(pos + num, pos);
        drawBuffer(num);

        return num;
    }

    public int readCharacter(final char... allowed) throws IOException {
      return readCharacter(false, allowed);
    }

    public int readCharacter(boolean checkForAltKeyCombo, 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(checkForAltKeyCombo)) < 0) {
            // nothing
        }

        return c;
    }

    /**
     * Read from the input stream and decode an operation from the key map.
     *
     * The input stream will be read character by character until a matching
     * binding can be found.  Characters that can't possibly be matched to
     * any binding will be discarded.
     *
     * @param keys the KeyMap to use for decoding the input stream
     * @return the decoded binding or <code>null</code> if the end of
     *         stream has been reached
     */
    public Object readBinding(KeyMap keys) throws IOException {
        Object o;
        opBuffer.setLength(0);
        do {
            int c = pushBackChar.isEmpty() ? readCharacter() : pushBackChar.pop();
            if (c == -1) {
                return null;
            }
            opBuffer.appendCodePoint(c);

            if (recording) {
                macro += new String(Character.toChars(c));
            }

            if (quotedInsert) {
                o = Operation.SELF_INSERT;
                quotedInsert = false;
            } else {
                o = keys.getBound(opBuffer);
            }

            /*
             * The kill ring keeps record of whether or not the
             * previous command was a yank or a kill. We reset
             * that state here if needed.
             */
            if (!recording && !(o instanceof KeyMap)) {
                if (o != Operation.YANK_POP && o != Operation.YANK) {
                    killRing.resetLastYank();
                }
                if (o != Operation.KILL_LINE && o != Operation.KILL_WHOLE_LINE
                        && o != Operation.BACKWARD_KILL_WORD && o != Operation.KILL_WORD
                        && o != Operation.UNIX_LINE_DISCARD && o != Operation.UNIX_WORD_RUBOUT) {
                    killRing.resetLastKill();
                }
            }

            if (o == Operation.DO_LOWERCASE_VERSION) {
                opBuffer.setLength(opBuffer.length() - 1);
                opBuffer.append(Character.toLowerCase((char) c));
                o = keys.getBound(opBuffer);
            }

            /*
             * A KeyMap indicates that the key that was struck has a
             * number of keys that can follow it as indicated in the
             * map. This is used primarily for Emacs style ESC-META-x
             * lookups. Since more keys must follow, go back to waiting
             * for the next key.
             */
            if (o instanceof KeyMap) {
                /*
                 * The ESC key (#27) is special in that it is ambiguous until
                 * you know what is coming next.  The ESC could be a literal
                 * escape, like the user entering vi-move mode, or it could
                 * be part of a terminal control sequence.  The following
                 * logic attempts to disambiguate things in the same
                 * fashion as regular vi or readline.
                 *
                 * When ESC is encountered and there is no other pending
                 * character in the pushback queue, then attempt to peek
                 * into the input stream (if the feature is enabled) for
                 * 150ms. If nothing else is coming, then assume it is
                 * not a terminal control sequence, but a raw escape.
                 */
                if (c == ESCAPE
                        && pushBackChar.isEmpty()
                        && in.isNonBlockingEnabled()
                        && in.peek(escapeTimeout) == READ_EXPIRED) {
                    o = ((KeyMap) o).getAnotherKey();
                    if (o == null || o instanceof KeyMap) {
                        continue;
                    }
                    opBuffer.setLength(0);
                } else {
                    continue;
                }
            }

            /*
             * If we didn't find a binding for the key and there is
             * more than one character accumulated then start checking
             * the largest span of characters from the beginning to
             * see if there is a binding for them.
             *
             * For example if our buffer has ESC,CTRL-M,C the getBound()
             * called previously indicated that there is no binding for
             * this sequence, so this then checks ESC,CTRL-M, and failing
             * that, just ESC. Each keystroke that is pealed off the end
             * during these tests is stuffed onto the pushback buffer so
             * they won't be lost.
             *
             * If there is no binding found, then we go back to waiting for
             * input.
             */
            while (o == null && opBuffer.length() > 0) {
                c = opBuffer.charAt(opBuffer.length() - 1);
                opBuffer.setLength(opBuffer.length() - 1);
                Object o2 = keys.getBound(opBuffer);
                if (o2 instanceof KeyMap) {
                    o = ((KeyMap) o2).getAnotherKey();
                    if (o == null) {
                        continue;
                    } else {
                        pushBackChar.push((char) c);
                    }
                }
            }

        } while (o == null || o instanceof KeyMap);

        return o;
    }

    public String getLastBinding() {
        return opBuffer.toString();
    }

    //
    // 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);
    }

    /**
     * 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 {
        return readLine(prompt, mask, 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 <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, 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.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.enableInterruptCharacter();
            }
        }
    }

    /**
     * Read a line for unsupported terminals.
     */
    private String readLineSimple() throws IOException {

        if (skipLF) {
            skipLF = false;

            int i = readCharacter();

            if (i == -1 || i == '\r') {
                return finishBuffer();
            } else if (i == '\n') {
                // ignore
            } else {
                buf.buffer.append((char) i);
            }
        }

        while (true) {
            int i = readCharacter();

            if (i == -1 && buf.buffer.length() == 0) {
              return null;
            }

            if (i == -1 || i == '\n') {
                return finishBuffer();
            } else if (i == '\r') {
                skipLF = true;
                return finishBuffer();
            } else {
                buf.buffer.append((char) i);
            }
        }
    }

    //
    // Completion
    //

    private final List<Completer> completers = new LinkedList<Completer>();

    private CompletionHandler completionHandler = new CandidateListCompletionHandler();

    /**
     * Add the specified {@link jline.console.completer.Completer} to the list of handlers for tab-completion.
     *
     * @param completer the {@link jline.console.completer.Completer} to add
     * @return true if it was successfully added
     */
    public boolean addCompleter(final Completer completer) {
        return completers.add(completer);
    }

    /**
     * Remove the specified {@link jline.console.completer.Completer} from the list of handlers for tab-completion.
     *
     * @param completer     The {@link Completer} to remove
     * @return              True if it was successfully removed
     */
    public boolean removeCompleter(final Completer completer) {
        return completers.remove(completer);
    }

    /**
     * Returns an unmodifiable list of all the completers.
     */
    public Collection<Completer> getCompleters() {
        return Collections.unmodifiableList(completers);
    }

    public void setCompletionHandler(final CompletionHandler handler) {
        this.completionHandler = checkNotNull(handler);
    }

    public CompletionHandler getCompletionHandler() {
        return this.completionHandler;
    }

    /**
     * Use the completers to modify the buffer with the appropriate completions.
     *
     * @return true if successful
     */
    protected 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 (Completer comp : completers) {
            if ((position = comp.complete(bufstr, cursor, candidates)) != -1) {
                break;
            }
        }

        return candidates.size() != 0 && getCompletionHandler().complete(this, candidates, position);
    }

    protected void printCompletionCandidates() throws IOException {
        // debug ("tab for (" + buf + ")");
        if (completers.size() == 0) {
            return;
        }

        List<CharSequence> candidates = new LinkedList<CharSequence>();
        String bufstr = buf.buffer.toString();
        int cursor = buf.cursor;

        for (Completer comp : completers) {
            if (comp.complete(bufstr, cursor, candidates) != -1) {
                break;
            }
        }
        CandidateListCompletionHandler.printCandidates(this, candidates);
        drawLine();
    }

    /**
     * The number of tab-completion candidates above which a warning will be
     * prompted before showing all the candidates.
     */
    private int autoprintThreshold = Configuration.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 History history = new MemoryHistory();

    public void setHistory(final History history) {
        this.history = history;
    }

    public 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;
    }

    /**
     * Used in "vi" mode for argumented history move, to move a specific
     * number of history entries forward or back.
     *
     * @param next If true, move forward
     * @param count The number of entries to move
     * @return true if the move was successful
     */
    private boolean moveHistory(final boolean next, int count) throws IOException {
        boolean ok = true;
        for (int i = 0; i < count && (ok = moveHistory(next)); i++) {
            /* empty */
        }
        return ok;
    }

    /**
     * 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
    //

    /**
     * Output the specified characters to the output stream without manipulating the current buffer.
     */
    private int fmtPrint(final CharSequence buff, int cursorPos) throws IOException {
        return fmtPrint(buff, 0, buff.length(), cursorPos);
    }

    private int fmtPrint(final CharSequence buff, int start, int end) throws IOException {
        return fmtPrint(buff, start, end, getCursorPosition());
    }

    private int fmtPrint(final CharSequence buff, int start, int end, int cursorPos) throws IOException {
        checkNotNull(buff);
        for (int i = start; i < end; i++) {
            char c = buff.charAt(i);
            if (c == '\t') {
                int nb = nextTabStop(cursorPos);
                cursorPos += nb;
                while (nb-- > 0) {
                    out.write(' ');
                }
            } else if (c < 32) {
                out.write('^');
                out.write((char) (c + '@'));
                cursorPos += 2;
            } else {
                int w = WCWidth.wcwidth(c);
                if (w > 0) {
                    out.write(c);
                    cursorPos += w;
                }
            }
        }
        cursorOk = false;
        return cursorPos;
    }

    /**
     * Output the specified string to the output stream (but not the buffer).
     */
    public void print(final CharSequence s) throws IOException {
        rawPrint(s.toString());
    }

    public void println(final CharSequence s) throws IOException {
        print(s);
        println();
    }

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    /**
     * Output a platform-dependant newline.
     */
    public void println() throws IOException {
        rawPrint(LINE_SEPARATOR);
    }

    /**
     * Raw output printing
     */
    final void rawPrint(final int c) throws IOException {
        out.write(c);
        cursorOk = false;
    }

    final void rawPrint(final String str) throws IOException {
        out.write(str);
        cursorOk = false;
    }

    private void rawPrint(final char c, final int num) throws IOException {
        for (int i = 0; i < num; i++) {
            rawPrint(c);
        }
    }

    private void rawPrintln(final String s) throws IOException {
        rawPrint(s);
        println();
    }


    //
    // Actions
    //

    /**
     * Issue a delete.
     *
     * @return true if successful
     */
    public boolean delete() throws IOException {
        if (buf.cursor == buf.buffer.length()) {
          return false;
        }

        buf.buffer.delete(buf.cursor, buf.cursor + 1);
        drawBuffer(1);

        return true;
    }

    /**
     * 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 = len - cp;
        int pos = getCursorPosition();
        int width = wcwidth(buf.buffer, cp, len, pos);
        clearAhead(width, pos);

        char[] killed = new char[num];
        buf.buffer.getChars(cp, (cp + num), killed, 0);
        buf.buffer.delete(cp, (cp + num));

        String copy = new String(killed);
        killRing.add(copy);

        return true;
    }

    public boolean yank() throws IOException {
        String yanked = killRing.yank();

        if (yanked == null) {
            return false;
        }
        putString(yanked);
        return true;
    }

    public boolean yankPop() throws IOException {
        if (!killRing.lastYank()) {
            return false;
        }
        String current = killRing.yank();
        if (current == null) {
            // This shouldn't happen.
            return false;
        }
        backspace(current.length());
        String yanked = killRing.yankPop();
        if (yanked == null) {
            // This shouldn't happen.
            return false;
        }

        putString(yanked);
        return true;
    }

    /**
     * Clear the screen by issuing the ANSI "clear screen" code.
     */
    public boolean clearScreen() throws IOException {
        if (!tputs("clear_screen")) {
            println();
        }
        return true;
    }

    /**
     * Issue an audible keyboard bell.
     */
    public void beep() throws IOException {
        if (bellEnabled) {
            if (tputs("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 {
            @SuppressWarnings("deprecation")
            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) {
            Log.error("Paste failed: ", e);

            return false;
        }
    }

    /**
     * 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) {
        getKeys().bind(Character.toString(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) {
            // we use 0 here, as we don't really support tabulations inside candidates
            int len = wcwidth(Ansi.stripAnsi(item.toString()), 0);
            maxWidth = Math.max(maxWidth, len);
        }
        maxWidth = maxWidth + 3;
        Log.debug("Max width: ", maxWidth);

        int showLines;
        if (isPaginationEnabled()) {
            showLines = height - 1; // page limit
        }
        else {
            showLines = Integer.MAX_VALUE;
        }

        StringBuilder buff = new StringBuilder();
        int realLength = 0;
        for (CharSequence item : items) {
            if ((realLength + maxWidth) > width) {
                rawPrintln(buff.toString());
                buff.setLength(0);
                realLength = 0;

                if (--showLines == 0) {
                    // Overflow
                    print(resources.getString("DISPLAY_MORE"));
                    flush();
                    int c = readCharacter();
                    if (c == '\r' || c == '\n') {
                        // one step forward
                        showLines = 1;
                    }
                    else if (c != 'q') {
                        // page forward
                        showLines = height - 1;
                    }

                    tputs("carriage_return");
                    if (c == 'q') {
                        // cancel
                        break;
                    }
                }
            }

            // NOTE: toString() is important here due to AnsiString being retarded
            buff.append(item.toString());
            int strippedItemLength = wcwidth(Ansi.stripAnsi(item.toString()), 0);
            for (int i = 0; i < (maxWidth - strippedItemLength); i++) {
                buff.append(' ');
            }
            realLength += maxWidth;
        }

        if (buff.length() > 0) {
            rawPrintln(buff.toString());
        }
    }

    //
    // 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()
            {
                public void run() {
                    while (!interrupted()) {
                        try {
                            Writer out = getOutput();
                            out.write(fullPrompt);
                            out.flush();
                            sleep(3);
                        }
                        catch (IOException e) {
                            return;
                        }
                        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(String prompt, String buffer, int cursorDest) throws IOException {
        // move cursor to end of line
        moveToEnd();

        // backspace all text, including prompt
        buf.buffer.append(this.prompt);
        int promptLength = 0;
        if (this.prompt != null) {
            promptLength = this.prompt.length();
        }

        buf.cursor += promptLength;
        setPrompt("");
        backspaceAll();

        setPrompt(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(String searchTerm, String match) throws IOException {
        printSearchStatus(searchTerm, match, "(reverse-i-search)`");
    }

    public void printForwardSearchStatus(String searchTerm, String match) throws IOException {
        printSearchStatus(searchTerm, match, "(i-search)`");
    }

    private void printSearchStatus(String searchTerm, String match, String searchLabel) throws IOException {
        String prompt = searchLabel + searchTerm + "': ";
        int cursorDest = match.indexOf(searchTerm);
        resetPromptLine(prompt, match, cursorDest);
    }

    public void restoreLine(String originalPrompt, 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(String searchTerm, 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(String searchTerm) {
        return searchBackwards(searchTerm, history.index());
    }


    public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) {
        ListIterator<History.Entry> it = history.entries(startIndex);
        while (it.hasPrevious()) {
            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;
    }

    /**
     * Search forward 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 searchForwards(String searchTerm, int startIndex) {
        return searchForwards(searchTerm, startIndex, false);
    }
    /**
     * Search forwards 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 searchForwards(String searchTerm) {
        return searchForwards(searchTerm, history.index());
    }

    public int searchForwards(String searchTerm, int startIndex, boolean startsWith) {
        if (startIndex >= history.size()) {
            startIndex = history.size() - 1;
        }

        ListIterator<History.Entry> it = history.entries(startIndex);

        if (searchIndex != -1 && it.hasNext()) {
            it.next();
        }

        while (it.hasNext()) {
            History.Entry e = it.next();
            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 static boolean isDelimiter(final char c) {
        return !Character.isLetterOrDigit(c);
    }

    /**
     * Checks to see if a character is a whitespace character. Currently
     * this delegates to {@link Character#isWhitespace(char)}, however
     * eventually it should be hooked up so that the definition of whitespace
     * can be configured, as readline does.
     *
     * @param c The character to check
     * @return true if the character is a whitespace
     */
    private static boolean isWhitespace(final char c) {
        return Character.isWhitespace (c);
    }

    private boolean tputs(String cap, Object... params) throws IOException {
        String str = terminal.getStringCapability(cap);
        if (str == null) {
            return false;
        }
        Curses.tputs(out, str, params);
        return true;
    }

}
