All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.pkl.thirdparty.jline.reader.impl.LineReaderImpl Maven / Gradle / Ivy

Go to download

Fat Jar containing pkl-cli, pkl-codegen-java, pkl-codegen-kotlin, pkl-config-java, pkl-core, pkl-doc, and their shaded third-party dependencies.

There is a newer version: 0.27.1
Show newest version
/*
 * Copyright (c) 2002-2022, the original author(s).
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package org.pkl.thirdparty.jline.reader.impl;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.Flushable;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.lang.reflect.Constructor;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.pkl.thirdparty.jline.keymap.BindingReader;
import org.pkl.thirdparty.jline.keymap.KeyMap;
import org.pkl.thirdparty.jline.reader.*;
import org.pkl.thirdparty.jline.reader.Parser.ParseContext;
import org.pkl.thirdparty.jline.reader.impl.history.DefaultHistory;
import org.pkl.thirdparty.jline.terminal.*;
import org.pkl.thirdparty.jline.terminal.Attributes.ControlChar;
import org.pkl.thirdparty.jline.terminal.Terminal.Signal;
import org.pkl.thirdparty.jline.terminal.Terminal.SignalHandler;
import org.pkl.thirdparty.jline.terminal.impl.AbstractWindowsTerminal;
import org.pkl.thirdparty.jline.utils.AttributedString;
import org.pkl.thirdparty.jline.utils.AttributedStringBuilder;
import org.pkl.thirdparty.jline.utils.AttributedStyle;
import org.pkl.thirdparty.jline.utils.Curses;
import org.pkl.thirdparty.jline.utils.Display;
import org.pkl.thirdparty.jline.utils.InfoCmp.Capability;
import org.pkl.thirdparty.jline.utils.Log;
import org.pkl.thirdparty.jline.utils.Status;
import org.pkl.thirdparty.jline.utils.StyleResolver;
import org.pkl.thirdparty.jline.utils.WCWidth;

import static org.pkl.thirdparty.jline.keymap.KeyMap.alt;
import static org.pkl.thirdparty.jline.keymap.KeyMap.ctrl;
import static org.pkl.thirdparty.jline.keymap.KeyMap.del;
import static org.pkl.thirdparty.jline.keymap.KeyMap.esc;
import static org.pkl.thirdparty.jline.keymap.KeyMap.range;
import static org.pkl.thirdparty.jline.keymap.KeyMap.translate;
import static org.pkl.thirdparty.jline.terminal.TerminalBuilder.PROP_DISABLE_ALTERNATE_CHARSET;

/**
 * A reader for terminal applications. It supports custom tab-completion,
 * saveable command history, and command line editing.
 *
 * @author Marc Prud'hommeaux
 * @author Jason Dillon
 * @author Guillaume Nodet
 */
@SuppressWarnings("StatementWithEmptyBody")
public class LineReaderImpl implements LineReader, Flushable {
    public static final char NULL_MASK = 0;

    public static final int TAB_WIDTH = 4;

    public static final String DEFAULT_WORDCHARS = "*?_-.[]~=/&;!#$%^(){}<>";
    public static final String DEFAULT_REMOVE_SUFFIX_CHARS = " \t\n;&|";
    public static final String DEFAULT_COMMENT_BEGIN = "#";
    public static final String DEFAULT_SEARCH_TERMINATORS = "\033\012";
    public static final String DEFAULT_BELL_STYLE = "";
    public static final int DEFAULT_LIST_MAX = 100;
    public static final int DEFAULT_MENU_LIST_MAX = Integer.MAX_VALUE;
    public static final int DEFAULT_ERRORS = 2;
    public static final long DEFAULT_BLINK_MATCHING_PAREN = 500L;
    public static final long DEFAULT_AMBIGUOUS_BINDING = 1000L;
    public static final String DEFAULT_SECONDARY_PROMPT_PATTERN = "%M> ";
    public static final String DEFAULT_OTHERS_GROUP_NAME = "others";
    public static final String DEFAULT_ORIGINAL_GROUP_NAME = "original";
    public static final String DEFAULT_COMPLETION_STYLE_STARTING = "fg:cyan";
    public static final String DEFAULT_COMPLETION_STYLE_DESCRIPTION = "fg:bright-black";
    public static final String DEFAULT_COMPLETION_STYLE_GROUP = "fg:bright-magenta,bold";
    public static final String DEFAULT_COMPLETION_STYLE_SELECTION = "inverse";
    public static final String DEFAULT_COMPLETION_STYLE_BACKGROUND = "bg:default";
    public static final String DEFAULT_COMPLETION_STYLE_LIST_STARTING = DEFAULT_COMPLETION_STYLE_STARTING;
    public static final String DEFAULT_COMPLETION_STYLE_LIST_DESCRIPTION = DEFAULT_COMPLETION_STYLE_DESCRIPTION;
    public static final String DEFAULT_COMPLETION_STYLE_LIST_GROUP = "fg:black,bold";
    public static final String DEFAULT_COMPLETION_STYLE_LIST_SELECTION = DEFAULT_COMPLETION_STYLE_SELECTION;
    public static final String DEFAULT_COMPLETION_STYLE_LIST_BACKGROUND = "bg:bright-magenta";
    public static final int DEFAULT_INDENTATION = 0;
    public static final int DEFAULT_FEATURES_MAX_BUFFER_SIZE = 1000;
    public static final int DEFAULT_SUGGESTIONS_MIN_BUFFER_SIZE = 1;

    private static final int MIN_ROWS = 3;

    public static final String BRACKETED_PASTE_ON = "\033[?2004h";
    public static final String BRACKETED_PASTE_OFF = "\033[?2004l";
    public static final String BRACKETED_PASTE_BEGIN = "\033[200~";
    public static final String BRACKETED_PASTE_END = "\033[201~";

    public static final String FOCUS_IN_SEQ = "\033[I";
    public static final String FOCUS_OUT_SEQ = "\033[O";

    /**
     * Possible states in which the current readline operation may be in.
     */
    protected enum State {
        /**
         * The user is just typing away
         */
        NORMAL,
        /**
         * readLine should exit and return the buffer content
         */
        DONE,
        /**
         * readLine should exit and return empty String
         */
        IGNORE,
        /**
         * readLine should exit and throw an EOFException
         */
        EOF,
        /**
         * readLine should exit and throw an UserInterruptException
         */
        INTERRUPT
    }

    protected enum ViMoveMode {
        NORMAL,
        YANK,
        DELETE,
        CHANGE
    }

    protected enum BellType {
        NONE,
        AUDIBLE,
        VISIBLE
    }

    //
    // Constructor variables
    //

    /** The terminal to use */
    protected final Terminal terminal;
    /** The application name */
    protected final String appName;
    /** The terminal keys mapping */
    protected final Map> keyMaps;

    //
    // Configuration
    //
    protected final Map variables;
    protected History history = new DefaultHistory();
    protected Completer completer = null;
    protected Highlighter highlighter = new DefaultHighlighter();
    protected Parser parser = new DefaultParser();
    protected Expander expander = new DefaultExpander();
    protected CompletionMatcher completionMatcher = new CompletionMatcherImpl();

    //
    // State variables
    //

    protected final Map options = new HashMap<>();

    protected final Buffer buf = new BufferImpl();
    protected String tailTip = "";
    protected SuggestionType autosuggestion = SuggestionType.NONE;

    protected final Size size = new Size();

    protected AttributedString prompt = AttributedString.EMPTY;
    protected AttributedString rightPrompt = AttributedString.EMPTY;

    protected MaskingCallback maskingCallback;

    protected Map modifiedHistory = new HashMap<>();
    protected Buffer historyBuffer = null;
    protected CharSequence searchBuffer;
    protected StringBuffer searchTerm = null;
    protected boolean searchFailing;
    protected boolean searchBackward;
    protected int searchIndex = -1;
    protected boolean doAutosuggestion;

    // Reading buffers
    protected final BindingReader bindingReader;

    /**
     * VI character find
     */
    protected int findChar;

    protected int findDir;
    protected int findTailAdd;
    /**
     * VI history string search
     */
    private int searchDir;

    private String searchString;

    /**
     * Region state
     */
    protected int regionMark;

    protected RegionType regionActive;

    private boolean forceChar;
    private boolean forceLine;

    /**
     * The vi yank buffer
     */
    protected String yankBuffer = "";

    protected ViMoveMode viMoveMode = ViMoveMode.NORMAL;

    protected KillRing killRing = new KillRing();

    protected UndoTree undo = new UndoTree<>(this::setBuffer);
    protected boolean isUndo;

    /**
     * State lock
     */
    protected final ReentrantLock lock = new ReentrantLock();
    /*
     * Current internal state of the line reader
     */
    protected State state = State.DONE;
    protected final AtomicBoolean startedReading = new AtomicBoolean();
    protected boolean reading;

    protected Supplier post;

    protected Map builtinWidgets;
    protected Map widgets;

    protected int count;
    protected int mult;
    protected int universal = 4;
    protected int repeatCount;
    protected boolean isArgDigit;

    protected ParsedLine parsedLine;

    protected boolean skipRedisplay;
    protected Display display;

    protected boolean overTyping = false;

    protected String keyMap;

    protected int smallTerminalOffset = 0;
    /*
     * accept-and-infer-next-history, accept-and-hold & accept-line-and-down-history
     */
    protected boolean nextCommandFromHistory = false;
    protected int nextHistoryId = -1;

    /*
     * execute commands from commandsBuffer
     */
    protected List commandsBuffer = new ArrayList<>();

    protected int candidateStartPosition = 0;

    protected String alternateIn;
    protected String alternateOut;

    public LineReaderImpl(Terminal terminal) throws IOException {
        this(terminal, terminal.getName(), null);
    }

    public LineReaderImpl(Terminal terminal, String appName) throws IOException {
        this(terminal, appName, null);
    }

    public LineReaderImpl(Terminal terminal, String appName, Map variables) {
        Objects.requireNonNull(terminal, "terminal can not be null");
        this.terminal = terminal;
        if (appName == null) {
            appName = "JLine";
        }
        this.appName = appName;
        if (variables != null) {
            this.variables = variables;
        } else {
            this.variables = new HashMap<>();
        }
        this.keyMaps = defaultKeyMaps();
        if (!Boolean.getBoolean(PROP_DISABLE_ALTERNATE_CHARSET)) {
            this.alternateIn = Curses.tputs(terminal.getStringCapability(Capability.enter_alt_charset_mode));
            this.alternateOut = Curses.tputs(terminal.getStringCapability(Capability.exit_alt_charset_mode));
        }

        builtinWidgets = builtinWidgets();
        widgets = new HashMap<>(builtinWidgets);
        bindingReader = new BindingReader(terminal.reader());
        doDisplay();
    }

    public Terminal getTerminal() {
        return terminal;
    }

    public String getAppName() {
        return appName;
    }

    public Map> getKeyMaps() {
        return keyMaps;
    }

    public KeyMap getKeys() {
        return keyMaps.get(keyMap);
    }

    @Override
    public Map getWidgets() {
        return widgets;
    }

    @Override
    public Map getBuiltinWidgets() {
        return Collections.unmodifiableMap(builtinWidgets);
    }

    @Override
    public Buffer getBuffer() {
        return buf;
    }

    @Override
    public void setAutosuggestion(SuggestionType type) {
        this.autosuggestion = type;
    }

    @Override
    public SuggestionType getAutosuggestion() {
        return autosuggestion;
    }

    @Override
    public String getTailTip() {
        return tailTip;
    }

    @Override
    public void setTailTip(String tailTip) {
        this.tailTip = tailTip;
    }

    @Override
    public void runMacro(String macro) {
        bindingReader.runMacro(macro);
    }

    @Override
    public MouseEvent readMouseEvent() {
        return terminal.readMouseEvent(bindingReader::readCharacter);
    }

    /**
     * Set the completer.
     *
     * @param completer the completer to use
     */
    public void setCompleter(Completer completer) {
        this.completer = completer;
    }

    /**
     * Returns the completer.
     *
     * @return the completer
     */
    public Completer getCompleter() {
        return completer;
    }

    //
    // History
    //

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

    public History getHistory() {
        return history;
    }

    //
    // Highlighter
    //

    public void setHighlighter(Highlighter highlighter) {
        this.highlighter = highlighter;
    }

    public Highlighter getHighlighter() {
        return highlighter;
    }

    public Parser getParser() {
        return parser;
    }

    public void setParser(Parser parser) {
        this.parser = parser;
    }

    @Override
    public Expander getExpander() {
        return expander;
    }

    public void setExpander(Expander expander) {
        this.expander = expander;
    }

    public void setCompletionMatcher(CompletionMatcher completionMatcher) {
        this.completionMatcher = completionMatcher;
    }

    //
    // Line Reading
    //

    /**
     * Read the next line and return the contents of the buffer.
     *
     * @return          A line that is read from the terminal, can never be null.
     */
    public String readLine() throws UserInterruptException, EndOfFileException {
        return readLine(null, null, (MaskingCallback) null, 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.
     *
     * @param mask      The mask character, null or 0.
     * @return          A line that is read from the terminal, can never be null.
     */
    public String readLine(Character mask) throws UserInterruptException, EndOfFileException {
        return readLine(null, null, mask, null);
    }

    /**
     * Read a line from the in {@link InputStream}, and return the line
     * (without any trailing newlines).
     *
     * @param prompt    The prompt to issue to the terminal, may be null.
     * @return          A line that is read from the terminal, can never be null.
     */
    public String readLine(String prompt) throws UserInterruptException, EndOfFileException {
        return readLine(prompt, null, (MaskingCallback) null, null);
    }

    /**
     * Read a line from the in {@link InputStream}, and return the line
     * (without any trailing newlines).
     *
     * @param prompt    The prompt to issue to the terminal, may be null.
     * @param mask      The mask character, null or 0.
     * @return          A line that is read from the terminal, can never be null.
     */
    public String readLine(String prompt, Character mask) throws UserInterruptException, EndOfFileException {
        return readLine(prompt, null, mask, null);
    }

    /**
     * Read a line from the in {@link InputStream}, and return the line
     * (without any trailing newlines).
     *
     * @param prompt    The prompt to issue to the terminal, may be null.
     * @param mask      The mask character, null or 0.
     * @param buffer    A string that will be set for editing.
     * @return          A line that is read from the terminal, can never be null.
     */
    public String readLine(String prompt, Character mask, String buffer)
            throws UserInterruptException, EndOfFileException {
        return readLine(prompt, null, mask, buffer);
    }

    /**
     * Read a line from the in {@link InputStream}, and return the line
     * (without any trailing newlines).
     *
     * @param prompt      The prompt to issue to the terminal, may be null.
     * @param rightPrompt The prompt to issue to the right of the terminal, may be null.
     * @param mask        The mask character, null or 0.
     * @param buffer      A string that will be set for editing.
     * @return            A line that is read from the terminal, can never be null.
     */
    public String readLine(String prompt, String rightPrompt, Character mask, String buffer)
            throws UserInterruptException, EndOfFileException {
        return readLine(prompt, rightPrompt, mask != null ? new SimpleMaskingCallback(mask) : null, buffer);
    }

    /**
     * Read a line from the in {@link InputStream}, and return the line
     * (without any trailing newlines).
     *
     * @param prompt          The prompt to issue to the terminal, may be null.
     * @param rightPrompt     The prompt to issue to the right of the terminal, may be null.
     * @param maskingCallback The callback used to mask parts of the edited line.
     * @param buffer          A string that will be set for editing.
     * @return                A line that is read from the terminal, can never be null.
     */
    public String readLine(String prompt, String rightPrompt, MaskingCallback maskingCallback, String buffer)
            throws UserInterruptException, EndOfFileException {
        // prompt may be null
        // maskingCallback may be null
        // buffer may be null
        if (!commandsBuffer.isEmpty()) {
            String cmd = commandsBuffer.remove(0);
            boolean done = false;
            do {
                try {
                    parser.parse(cmd, cmd.length() + 1, ParseContext.ACCEPT_LINE);
                    done = true;
                } catch (EOFError e) {
                    if (commandsBuffer.isEmpty()) {
                        throw new IllegalArgumentException("Incompleted command: \n" + cmd);
                    }
                    cmd += "\n";
                    cmd += commandsBuffer.remove(0);
                } catch (SyntaxError e) {
                    done = true;
                } catch (Exception e) {
                    commandsBuffer.clear();
                    throw new IllegalArgumentException(e.getMessage());
                }
            } while (!done);
            AttributedStringBuilder sb = new AttributedStringBuilder();
            sb.styled(AttributedStyle::bold, cmd);
            sb.toAttributedString().println(terminal);
            terminal.flush();
            return finish(cmd);
        }

        if (!startedReading.compareAndSet(false, true)) {
            throw new IllegalStateException();
        }

        Thread readLineThread = Thread.currentThread();
        SignalHandler previousIntrHandler = null;
        SignalHandler previousWinchHandler = null;
        SignalHandler previousContHandler = null;
        Attributes originalAttributes = null;
        boolean dumb = isTerminalDumb();
        try {

            this.maskingCallback = maskingCallback;

            /*
             * 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.
             */
            repeatCount = 0;
            mult = 1;
            regionActive = RegionType.NONE;
            regionMark = -1;

            smallTerminalOffset = 0;

            state = State.NORMAL;

            modifiedHistory.clear();

            setPrompt(prompt);
            setRightPrompt(rightPrompt);
            buf.clear();
            if (buffer != null) {
                buf.write(buffer);
            }
            if (nextCommandFromHistory && nextHistoryId > 0) {
                if (history.size() > nextHistoryId) {
                    history.moveTo(nextHistoryId);
                } else {
                    history.moveTo(history.last());
                }
                buf.write(history.current());
            } else {
                nextHistoryId = -1;
            }
            nextCommandFromHistory = false;
            undo.clear();
            parsedLine = null;
            keyMap = MAIN;

            if (history != null) {
                history.attach(this);
            }

            try {
                lock.lock();

                this.reading = true;

                previousIntrHandler = terminal.handle(Signal.INT, signal -> readLineThread.interrupt());
                previousWinchHandler = terminal.handle(Signal.WINCH, this::handleSignal);
                previousContHandler = terminal.handle(Signal.CONT, this::handleSignal);
                originalAttributes = terminal.enterRawMode();

                doDisplay();

                // Move into application mode
                if (!dumb) {
                    terminal.puts(Capability.keypad_xmit);
                    if (isSet(Option.AUTO_FRESH_LINE)) callWidget(FRESH_LINE);
                    if (isSet(Option.MOUSE)) terminal.trackMouse(Terminal.MouseTracking.Normal);
                    if (isSet(Option.BRACKETED_PASTE)) terminal.writer().write(BRACKETED_PASTE_ON);
                } else {
                    // For dumb terminals, we need to make sure that CR are ignored
                    Attributes attr = new Attributes(originalAttributes);
                    attr.setInputFlag(Attributes.InputFlag.IGNCR, true);
                    terminal.setAttributes(attr);
                }

                callWidget(CALLBACK_INIT);

                if (!isSet(Option.DISABLE_UNDO)) undo.newState(buf.copy());

                // Draw initial prompt
                redrawLine();
                redisplay();
            } finally {
                lock.unlock();
            }

            while (true) {

                KeyMap local = null;
                if (isInViCmdMode() && regionActive != RegionType.NONE) {
                    local = keyMaps.get(VISUAL);
                }
                Binding o = readBinding(getKeys(), local);
                if (o == null) {
                    throw new EndOfFileException().partialLine(buf.length() > 0 ? buf.toString() : null);
                }
                Log.trace("Binding: ", o);
                if (buf.length() == 0
                        && getLastBinding().charAt(0) == originalAttributes.getControlChar(ControlChar.VEOF)) {
                    throw new EndOfFileException();
                }

                // If this is still false after handling the binding, then
                // we reset our repeatCount to 0.
                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.
                count = ((repeatCount == 0) ? 1 : repeatCount) * mult;
                // Reset undo/redo flag
                isUndo = false;
                // Reset region after a paste
                if (regionActive == RegionType.PASTE) {
                    regionActive = RegionType.NONE;
                }

                try {
                    lock.lock();
                    // Get executable widget
                    Buffer copy = buf.length() <= getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE)
                            ? buf.copy()
                            : null;
                    Widget w = getWidget(o);
                    if (!w.apply()) {
                        beep();
                    }
                    if (!isSet(Option.DISABLE_UNDO)
                            && !isUndo
                            && copy != null
                            && buf.length() <= getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE)
                            && !copy.toString().equals(buf.toString())) {
                        undo.newState(buf.copy());
                    }

                    switch (state) {
                        case DONE:
                            return finishBuffer();
                        case IGNORE:
                            return "";
                        case EOF:
                            throw new EndOfFileException();
                        case INTERRUPT:
                            throw new UserInterruptException(buf.toString());
                    }

                    if (!isArgDigit) {
                        /*
                         * If the operation performed wasn't a vi argument
                         * digit, then clear out the current repeatCount;
                         */
                        repeatCount = 0;
                        mult = 1;
                    }

                    if (!dumb) {
                        redisplay();
                    }
                } finally {
                    lock.unlock();
                }
            }
        } catch (IOError e) {
            if (e.getCause() instanceof InterruptedIOException) {
                throw new UserInterruptException(buf.toString());
            } else {
                throw e;
            }
        } finally {
            try {
                lock.lock();

                this.reading = false;

                cleanup();
                if (originalAttributes != null) {
                    terminal.setAttributes(originalAttributes);
                }
                if (previousIntrHandler != null) {
                    terminal.handle(Signal.INT, previousIntrHandler);
                }
                if (previousWinchHandler != null) {
                    terminal.handle(Signal.WINCH, previousWinchHandler);
                }
                if (previousContHandler != null) {
                    terminal.handle(Signal.CONT, previousContHandler);
                }
            } finally {
                lock.unlock();
                startedReading.set(false);
            }
        }
    }

    private boolean isTerminalDumb() {
        return Terminal.TYPE_DUMB.equals(terminal.getType()) || Terminal.TYPE_DUMB_COLOR.equals(terminal.getType());
    }

    private void doDisplay() {
        // Cache terminal size for the duration of the call to readLine()
        // It will eventually be updated with WINCH signals
        size.copy(terminal.getBufferSize());

        display = new Display(terminal, false);
        display.resize(size.getRows(), size.getColumns());
        if (isSet(Option.DELAY_LINE_WRAP)) display.setDelayLineWrap(true);
    }

    @Override
    public void printAbove(String str) {
        try {
            lock.lock();

            boolean reading = this.reading;
            if (reading) {
                display.update(Collections.emptyList(), 0);
            }
            if (str.endsWith("\n") || str.endsWith("\n\033[m") || str.endsWith("\n\033[0m")) {
                terminal.writer().print(str);
            } else {
                terminal.writer().println(str);
            }
            if (reading) {
                redisplay(false);
            }
            terminal.flush();
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void printAbove(AttributedString str) {
        printAbove(str.toAnsi(terminal));
    }

    @Override
    public boolean isReading() {
        try {
            lock.lock();
            return reading;
        } finally {
            lock.unlock();
        }
    }

    /* Make sure we position the cursor on column 0 */
    protected boolean freshLine() {
        boolean wrapAtEol = terminal.getBooleanCapability(Capability.auto_right_margin);
        boolean delayedWrapAtEol = wrapAtEol && terminal.getBooleanCapability(Capability.eat_newline_glitch);
        AttributedStringBuilder sb = new AttributedStringBuilder();
        sb.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.BLACK + AttributedStyle.BRIGHT));
        sb.append("~");
        sb.style(AttributedStyle.DEFAULT);
        if (!wrapAtEol || delayedWrapAtEol) {
            for (int i = 0; i < size.getColumns() - 1; i++) {
                sb.append(" ");
            }
            sb.append(KeyMap.key(terminal, Capability.carriage_return));
            sb.append(" ");
            sb.append(KeyMap.key(terminal, Capability.carriage_return));
        } else {
            // Given the terminal will wrap automatically,
            // we need to print one less than needed.
            // This means that the last character will not
            // be overwritten, and that's why we're using
            // a clr_eol first if possible.
            String el = terminal.getStringCapability(Capability.clr_eol);
            if (el != null) {
                Curses.tputs(sb, el);
            }
            for (int i = 0; i < size.getColumns() - 2; i++) {
                sb.append(" ");
            }
            sb.append(KeyMap.key(terminal, Capability.carriage_return));
            sb.append(" ");
            sb.append(KeyMap.key(terminal, Capability.carriage_return));
        }
        sb.print(terminal);
        return true;
    }

    @Override
    public void callWidget(String name) {
        try {
            lock.lock();
            if (!reading) {
                throw new IllegalStateException("Widgets can only be called during a `readLine` call");
            }
            try {
                Widget w;
                if (name.startsWith(".")) {
                    w = builtinWidgets.get(name.substring(1));
                } else {
                    w = widgets.get(name);
                }
                if (w != null) {
                    w.apply();
                }
            } catch (Throwable t) {
                Log.debug("Error executing widget '", name, "'", t);
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Clear the line and redraw it.
     * @return true
     */
    public boolean redrawLine() {
        display.reset();
        return true;
    }

    /**
     * Write out the specified string to the buffer and the output stream.
     * @param str the char sequence to write in the buffer
     */
    public void putString(final CharSequence str) {
        buf.write(str, overTyping);
    }

    /**
     * Flush the terminal output stream. This is important for printout out single
     * characters (like a buf.backspace or keyboard) that we want the terminal to
     * handle immediately.
     */
    public void flush() {
        terminal.flush();
    }

    public boolean isKeyMap(String name) {
        return keyMap.equals(name);
    }

    /**
     * Read a character from the terminal.
     *
     * @return the character, or -1 if an EOF is received.
     */
    public int readCharacter() {
        if (lock.isHeldByCurrentThread()) {
            try {
                lock.unlock();
                return bindingReader.readCharacter();
            } finally {
                lock.lock();
            }
        } else {
            return bindingReader.readCharacter();
        }
    }

    public int peekCharacter(long timeout) {
        return bindingReader.peekCharacter(timeout);
    }

    protected  T doReadBinding(KeyMap keys, KeyMap local) {
        if (lock.isHeldByCurrentThread()) {
            try {
                lock.unlock();
                return bindingReader.readBinding(keys, local);
            } finally {
                lock.lock();
            }
        } else {
            return bindingReader.readBinding(keys, local);
        }
    }

    protected String doReadStringUntil(String sequence) {
        if (lock.isHeldByCurrentThread()) {
            try {
                lock.unlock();
                return bindingReader.readStringUntil(sequence);
            } finally {
                lock.lock();
            }
        } else {
            return bindingReader.readStringUntil(sequence);
        }
    }

    /**
     * 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 null if the end of
     *         stream has been reached
     */
    public Binding readBinding(KeyMap keys) {
        return readBinding(keys, null);
    }

    public Binding readBinding(KeyMap keys, KeyMap local) {
        Binding o = doReadBinding(keys, local);
        /*
         * 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 (o instanceof Reference) {
            String ref = ((Reference) o).name();
            if (!YANK_POP.equals(ref) && !YANK.equals(ref)) {
                killRing.resetLastYank();
            }
            if (!KILL_LINE.equals(ref)
                    && !KILL_WHOLE_LINE.equals(ref)
                    && !BACKWARD_KILL_WORD.equals(ref)
                    && !KILL_WORD.equals(ref)) {
                killRing.resetLastKill();
            }
        }
        return o;
    }

    @Override
    public ParsedLine getParsedLine() {
        return parsedLine;
    }

    @Override
    public String getLastBinding() {
        return bindingReader.getLastBinding();
    }

    @Override
    public String getSearchTerm() {
        return searchTerm != null ? searchTerm.toString() : null;
    }

    @Override
    public RegionType getRegionActive() {
        return regionActive;
    }

    @Override
    public int getRegionMark() {
        return regionMark;
    }

    //
    // Key Bindings
    //

    /**
     * Sets the current keymap by name. Supported keymaps are "emacs",
     * "viins", "vicmd".
     * @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) {
        KeyMap map = keyMaps.get(name);
        if (map == null) {
            return false;
        }
        this.keyMap = name;
        if (reading) {
            callWidget(CALLBACK_KEYMAP);
        }
        return true;
    }

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

    @Override
    public LineReader variable(String name, Object value) {
        variables.put(name, value);
        return this;
    }

    @Override
    public Map getVariables() {
        return variables;
    }

    @Override
    public Object getVariable(String name) {
        return variables.get(name);
    }

    @Override
    public void setVariable(String name, Object value) {
        variables.put(name, value);
    }

    @Override
    public LineReader option(Option option, boolean value) {
        options.put(option, value);
        return this;
    }

    @Override
    public boolean isSet(Option option) {
        return option.isSet(options);
    }

    @Override
    public void setOpt(Option option) {
        options.put(option, Boolean.TRUE);
    }

    @Override
    public void unsetOpt(Option option) {
        options.put(option, Boolean.FALSE);
    }

    @Override
    public void addCommandsInBuffer(Collection commands) {
        commandsBuffer.addAll(commands);
    }

    @Override
    public void editAndAddInBuffer(File file) throws Exception {
        if (isSet(Option.BRACKETED_PASTE)) {
            terminal.writer().write(BRACKETED_PASTE_OFF);
        }
        Constructor ctor = Class.forName("org.pkl.thirdparty.jline.builtins.Nano").getConstructor(Terminal.class, File.class);
        Editor editor = (Editor) ctor.newInstance(terminal, new File(file.getParent()));
        editor.setRestricted(true);
        editor.open(Collections.singletonList(file.getName()));
        editor.run();
        try (BufferedReader br = new BufferedReader(new FileReader(file))) {
            String line;
            commandsBuffer.clear();
            while ((line = br.readLine()) != null) {
                commandsBuffer.add(line);
            }
        }
    }

    //
    // Widget implementation
    //

    /**
     * Clear the buffer and add its contents to the history.
     *
     * @return the former contents of the buffer.
     */
    protected String finishBuffer() {
        return finish(buf.toString());
    }

    protected String finish(String str) {
        String historyLine = str;

        if (!isSet(Option.DISABLE_EVENT_EXPANSION)) {
            StringBuilder sb = new StringBuilder();
            boolean escaped = false;
            for (int i = 0; i < str.length(); i++) {
                char ch = str.charAt(i);
                if (escaped) {
                    escaped = false;
                    if (ch != '\n') {
                        sb.append(ch);
                    }
                } else if (parser.isEscapeChar(ch)) {
                    escaped = true;
                } else {
                    sb.append(ch);
                }
            }
            str = sb.toString();
        }

        if (maskingCallback != null) {
            historyLine = maskingCallback.history(historyLine);
        }

        // we only add it to the history if the buffer is not empty
        if (historyLine != null && historyLine.length() > 0) {
            history.add(Instant.now(), historyLine);
        }
        return str;
    }

    protected void handleSignal(Signal signal) {
        doAutosuggestion = false;
        if (signal == Signal.WINCH) {
            Status status = Status.getStatus(terminal, false);
            if (status != null) {
                status.hardReset();
            }
            size.copy(terminal.getBufferSize());
            display.resize(size.getRows(), size.getColumns());
            // restores prompt but also prevents scrolling in consoleZ, see #492
            // redrawLine();
            redisplay();
        } else if (signal == Signal.CONT) {
            terminal.enterRawMode();
            size.copy(terminal.getBufferSize());
            display.resize(size.getRows(), size.getColumns());
            terminal.puts(Capability.keypad_xmit);
            redrawLine();
            redisplay();
        }
    }

    @SuppressWarnings("unchecked")
    protected Widget getWidget(Object binding) {
        Widget w;
        if (binding instanceof Widget) {
            w = (Widget) binding;
        } else if (binding instanceof Macro) {
            String macro = ((Macro) binding).getSequence();
            w = () -> {
                bindingReader.runMacro(macro);
                return true;
            };
        } else if (binding instanceof Reference) {
            String name = ((Reference) binding).name();
            w = widgets.get(name);
            if (w == null) {
                w = () -> {
                    post = () -> new AttributedString("No such widget `" + name + "'");
                    return false;
                };
            }
        } else {
            w = () -> {
                post = () -> new AttributedString("Unsupported widget");
                return false;
            };
        }
        return w;
    }

    //
    // Helper methods
    //

    public void setPrompt(final String prompt) {
        this.prompt = (prompt == null ? AttributedString.EMPTY : expandPromptPattern(prompt, 0, "", 0));
    }

    public void setRightPrompt(final String rightPrompt) {
        this.rightPrompt = (rightPrompt == null ? AttributedString.EMPTY : expandPromptPattern(rightPrompt, 0, "", 0));
    }

    protected void setBuffer(Buffer buffer) {
        buf.copyFrom(buffer);
    }

    /**
     * Set the current buffer's content to the specified {@link String}. The
     * visual terminal will be modified to show the current buffer.
     *
     * @param buffer the new contents of the buffer.
     */
    protected void setBuffer(final String buffer) {
        buf.clear();
        buf.write(buffer);
    }

    /**
     * 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
     */
    protected String viDeleteChangeYankToRemap(String op) {
        switch (op) {
            case SEND_BREAK:
            case BACKWARD_CHAR:
            case FORWARD_CHAR:
            case END_OF_LINE:
            case VI_MATCH_BRACKET:
            case VI_DIGIT_OR_BEGINNING_OF_LINE:
            case NEG_ARGUMENT:
            case DIGIT_ARGUMENT:
            case VI_BACKWARD_CHAR:
            case VI_BACKWARD_WORD:
            case VI_FORWARD_CHAR:
            case VI_FORWARD_WORD:
            case VI_FORWARD_WORD_END:
            case VI_FIRST_NON_BLANK:
            case VI_GOTO_COLUMN:
            case VI_DELETE:
            case VI_YANK:
            case VI_CHANGE:
            case VI_FIND_NEXT_CHAR:
            case VI_FIND_NEXT_CHAR_SKIP:
            case VI_FIND_PREV_CHAR:
            case VI_FIND_PREV_CHAR_SKIP:
            case VI_REPEAT_FIND:
            case VI_REV_REPEAT_FIND:
                return op;

            default:
                return VI_CMD_MODE;
        }
    }

    protected int switchCase(int ch) {
        if (Character.isUpperCase(ch)) {
            return Character.toLowerCase(ch);
        } else if (Character.isLowerCase(ch)) {
            return Character.toUpperCase(ch);
        } else {
            return ch;
        }
    }

    /**
     * @return true if line reader is in the middle of doing a change-to
     *   delete-to or yank-to.
     */
    protected boolean isInViMoveOperation() {
        return viMoveMode != ViMoveMode.NORMAL;
    }

    protected boolean isInViChangeOperation() {
        return viMoveMode == ViMoveMode.CHANGE;
    }

    protected boolean isInViCmdMode() {
        return VICMD.equals(keyMap);
    }

    //
    // Movement
    //

    protected boolean viForwardChar() {
        if (count < 0) {
            return callNeg(this::viBackwardChar);
        }
        int lim = findeol();
        if (isInViCmdMode() && !isInViMoveOperation()) {
            lim--;
        }
        if (buf.cursor() >= lim) {
            return false;
        }
        while (count-- > 0 && buf.cursor() < lim) {
            buf.move(1);
        }
        return true;
    }

    protected boolean viBackwardChar() {
        if (count < 0) {
            return callNeg(this::viForwardChar);
        }
        int lim = findbol();
        if (buf.cursor() == lim) {
            return false;
        }
        while (count-- > 0 && buf.cursor() > 0) {
            buf.move(-1);
            if (buf.currChar() == '\n') {
                buf.move(1);
                break;
            }
        }
        return true;
    }

    //
    // Word movement
    //

    protected boolean forwardWord() {
        if (count < 0) {
            return callNeg(this::backwardWord);
        }
        while (count-- > 0) {
            while (buf.cursor() < buf.length() && isWord(buf.currChar())) {
                buf.move(1);
            }
            if (isInViChangeOperation() && count == 0) {
                break;
            }
            while (buf.cursor() < buf.length() && !isWord(buf.currChar())) {
                buf.move(1);
            }
        }
        return true;
    }

    protected boolean viForwardWord() {
        if (count < 0) {
            return callNeg(this::viBackwardWord);
        }
        while (count-- > 0) {
            if (isViAlphaNum(buf.currChar())) {
                while (buf.cursor() < buf.length() && isViAlphaNum(buf.currChar())) {
                    buf.move(1);
                }
            } else {
                while (buf.cursor() < buf.length() && !isViAlphaNum(buf.currChar()) && !isWhitespace(buf.currChar())) {
                    buf.move(1);
                }
            }
            if (isInViChangeOperation() && count == 0) {
                return true;
            }
            int nl = buf.currChar() == '\n' ? 1 : 0;
            while (buf.cursor() < buf.length() && nl < 2 && isWhitespace(buf.currChar())) {
                buf.move(1);
                nl += buf.currChar() == '\n' ? 1 : 0;
            }
        }
        return true;
    }

    protected boolean viForwardBlankWord() {
        if (count < 0) {
            return callNeg(this::viBackwardBlankWord);
        }
        while (count-- > 0) {
            while (buf.cursor() < buf.length() && !isWhitespace(buf.currChar())) {
                buf.move(1);
            }
            if (isInViChangeOperation() && count == 0) {
                return true;
            }
            int nl = buf.currChar() == '\n' ? 1 : 0;
            while (buf.cursor() < buf.length() && nl < 2 && isWhitespace(buf.currChar())) {
                buf.move(1);
                nl += buf.currChar() == '\n' ? 1 : 0;
            }
        }
        return true;
    }

    protected boolean emacsForwardWord() {
        return forwardWord();
    }

    protected boolean viForwardBlankWordEnd() {
        if (count < 0) {
            return false;
        }
        while (count-- > 0) {
            while (buf.cursor() < buf.length()) {
                buf.move(1);
                if (!isWhitespace(buf.currChar())) {
                    break;
                }
            }
            while (buf.cursor() < buf.length()) {
                buf.move(1);
                if (isWhitespace(buf.currChar())) {
                    break;
                }
            }
        }
        return true;
    }

    protected boolean viForwardWordEnd() {
        if (count < 0) {
            return callNeg(this::backwardWord);
        }
        while (count-- > 0) {
            while (buf.cursor() < buf.length()) {
                if (!isWhitespace(buf.nextChar())) {
                    break;
                }
                buf.move(1);
            }
            if (buf.cursor() < buf.length()) {
                if (isViAlphaNum(buf.nextChar())) {
                    buf.move(1);
                    while (buf.cursor() < buf.length() && isViAlphaNum(buf.nextChar())) {
                        buf.move(1);
                    }
                } else {
                    buf.move(1);
                    while (buf.cursor() < buf.length()
                            && !isViAlphaNum(buf.nextChar())
                            && !isWhitespace(buf.nextChar())) {
                        buf.move(1);
                    }
                }
            }
        }
        if (buf.cursor() < buf.length() && isInViMoveOperation()) {
            buf.move(1);
        }
        return true;
    }

    protected boolean backwardWord() {
        if (count < 0) {
            return callNeg(this::forwardWord);
        }
        while (count-- > 0) {
            while (buf.cursor() > 0 && !isWord(buf.atChar(buf.cursor() - 1))) {
                buf.move(-1);
            }
            while (buf.cursor() > 0 && isWord(buf.atChar(buf.cursor() - 1))) {
                buf.move(-1);
            }
        }
        return true;
    }

    protected boolean viBackwardWord() {
        if (count < 0) {
            return callNeg(this::viForwardWord);
        }
        while (count-- > 0) {
            int nl = 0;
            while (buf.cursor() > 0) {
                buf.move(-1);
                if (!isWhitespace(buf.currChar())) {
                    break;
                }
                nl += buf.currChar() == '\n' ? 1 : 0;
                if (nl == 2) {
                    buf.move(1);
                    break;
                }
            }
            if (buf.cursor() > 0) {
                if (isViAlphaNum(buf.currChar())) {
                    while (buf.cursor() > 0) {
                        if (!isViAlphaNum(buf.prevChar())) {
                            break;
                        }
                        buf.move(-1);
                    }
                } else {
                    while (buf.cursor() > 0) {
                        if (isViAlphaNum(buf.prevChar()) || isWhitespace(buf.prevChar())) {
                            break;
                        }
                        buf.move(-1);
                    }
                }
            }
        }
        return true;
    }

    protected boolean viBackwardBlankWord() {
        if (count < 0) {
            return callNeg(this::viForwardBlankWord);
        }
        while (count-- > 0) {
            while (buf.cursor() > 0) {
                buf.move(-1);
                if (!isWhitespace(buf.currChar())) {
                    break;
                }
            }
            while (buf.cursor() > 0) {
                buf.move(-1);
                if (isWhitespace(buf.currChar())) {
                    break;
                }
            }
        }
        return true;
    }

    protected boolean viBackwardWordEnd() {
        if (count < 0) {
            return callNeg(this::viForwardWordEnd);
        }
        while (count-- > 0 && buf.cursor() > 1) {
            int start;
            if (isViAlphaNum(buf.currChar())) {
                start = 1;
            } else if (!isWhitespace(buf.currChar())) {
                start = 2;
            } else {
                start = 0;
            }
            while (buf.cursor() > 0) {
                boolean same = (start != 1) && isWhitespace(buf.currChar());
                if (start != 0) {
                    same |= isViAlphaNum(buf.currChar());
                }
                if (same == (start == 2)) {
                    break;
                }
                buf.move(-1);
            }
            while (buf.cursor() > 0 && isWhitespace(buf.currChar())) {
                buf.move(-1);
            }
        }
        return true;
    }

    protected boolean viBackwardBlankWordEnd() {
        if (count < 0) {
            return callNeg(this::viForwardBlankWordEnd);
        }
        while (count-- > 0) {
            while (buf.cursor() > 0 && !isWhitespace(buf.currChar())) {
                buf.move(-1);
            }
            while (buf.cursor() > 0 && isWhitespace(buf.currChar())) {
                buf.move(-1);
            }
        }
        return true;
    }

    protected boolean emacsBackwardWord() {
        return backwardWord();
    }

    protected boolean backwardDeleteWord() {
        if (count < 0) {
            return callNeg(this::deleteWord);
        }
        int cursor = buf.cursor();
        while (count-- > 0) {
            while (cursor > 0 && !isWord(buf.atChar(cursor - 1))) {
                cursor--;
            }
            while (cursor > 0 && isWord(buf.atChar(cursor - 1))) {
                cursor--;
            }
        }
        buf.backspace(buf.cursor() - cursor);
        return true;
    }

    protected boolean viBackwardKillWord() {
        if (count < 0) {
            return false;
        }
        int lim = findbol();
        int x = buf.cursor();
        while (count-- > 0) {
            while (x > lim && isWhitespace(buf.atChar(x - 1))) {
                x--;
            }
            if (x > lim) {
                if (isViAlphaNum(buf.atChar(x - 1))) {
                    while (x > lim && isViAlphaNum(buf.atChar(x - 1))) {
                        x--;
                    }
                } else {
                    while (x > lim && !isViAlphaNum(buf.atChar(x - 1)) && !isWhitespace(buf.atChar(x - 1))) {
                        x--;
                    }
                }
            }
        }
        killRing.addBackwards(buf.substring(x, buf.cursor()));
        buf.backspace(buf.cursor() - x);
        return true;
    }

    protected boolean backwardKillWord() {
        if (count < 0) {
            return callNeg(this::killWord);
        }
        int x = buf.cursor();
        while (count-- > 0) {
            while (x > 0 && !isWord(buf.atChar(x - 1))) {
                x--;
            }
            while (x > 0 && isWord(buf.atChar(x - 1))) {
                x--;
            }
        }
        killRing.addBackwards(buf.substring(x, buf.cursor()));
        buf.backspace(buf.cursor() - x);
        return true;
    }

    protected boolean copyPrevWord() {
        if (count <= 0) {
            return false;
        }
        int t1, t0 = buf.cursor();
        while (true) {
            t1 = t0;
            while (t0 > 0 && !isWord(buf.atChar(t0 - 1))) {
                t0--;
            }
            while (t0 > 0 && isWord(buf.atChar(t0 - 1))) {
                t0--;
            }
            if (--count == 0) {
                break;
            }
            if (t0 == 0) {
                return false;
            }
        }
        buf.write(buf.substring(t0, t1));
        return true;
    }

    protected boolean upCaseWord() {
        int count = Math.abs(this.count);
        int cursor = buf.cursor();
        while (count-- > 0) {
            while (buf.cursor() < buf.length() && !isWord(buf.currChar())) {
                buf.move(1);
            }
            while (buf.cursor() < buf.length() && isWord(buf.currChar())) {
                buf.currChar(Character.toUpperCase(buf.currChar()));
                buf.move(1);
            }
        }
        if (this.count < 0) {
            buf.cursor(cursor);
        }
        return true;
    }

    protected boolean downCaseWord() {
        int count = Math.abs(this.count);
        int cursor = buf.cursor();
        while (count-- > 0) {
            while (buf.cursor() < buf.length() && !isWord(buf.currChar())) {
                buf.move(1);
            }
            while (buf.cursor() < buf.length() && isWord(buf.currChar())) {
                buf.currChar(Character.toLowerCase(buf.currChar()));
                buf.move(1);
            }
        }
        if (this.count < 0) {
            buf.cursor(cursor);
        }
        return true;
    }

    protected boolean capitalizeWord() {
        int count = Math.abs(this.count);
        int cursor = buf.cursor();
        while (count-- > 0) {
            boolean first = true;
            while (buf.cursor() < buf.length() && !isWord(buf.currChar())) {
                buf.move(1);
            }
            while (buf.cursor() < buf.length() && isWord(buf.currChar()) && !isAlpha(buf.currChar())) {
                buf.move(1);
            }
            while (buf.cursor() < buf.length() && isWord(buf.currChar())) {
                buf.currChar(first ? Character.toUpperCase(buf.currChar()) : Character.toLowerCase(buf.currChar()));
                buf.move(1);
                first = false;
            }
        }
        if (this.count < 0) {
            buf.cursor(cursor);
        }
        return true;
    }

    protected boolean deleteWord() {
        if (count < 0) {
            return callNeg(this::backwardDeleteWord);
        }
        int x = buf.cursor();
        while (count-- > 0) {
            while (x < buf.length() && !isWord(buf.atChar(x))) {
                x++;
            }
            while (x < buf.length() && isWord(buf.atChar(x))) {
                x++;
            }
        }
        buf.delete(x - buf.cursor());
        return true;
    }

    protected boolean killWord() {
        if (count < 0) {
            return callNeg(this::backwardKillWord);
        }
        int x = buf.cursor();
        while (count-- > 0) {
            while (x < buf.length() && !isWord(buf.atChar(x))) {
                x++;
            }
            while (x < buf.length() && isWord(buf.atChar(x))) {
                x++;
            }
        }
        killRing.add(buf.substring(buf.cursor(), x));
        buf.delete(x - buf.cursor());
        return true;
    }

    protected boolean transposeWords() {
        int lstart = buf.cursor() - 1;
        int lend = buf.cursor();
        while (buf.atChar(lstart) != 0 && buf.atChar(lstart) != '\n') {
            lstart--;
        }
        lstart++;
        while (buf.atChar(lend) != 0 && buf.atChar(lend) != '\n') {
            lend++;
        }
        if (lend - lstart < 2) {
            return false;
        }
        int words = 0;
        boolean inWord = false;
        if (!isDelimiter(buf.atChar(lstart))) {
            words++;
            inWord = true;
        }
        for (int i = lstart; i < lend; i++) {
            if (isDelimiter(buf.atChar(i))) {
                inWord = false;
            } else {
                if (!inWord) {
                    words++;
                }
                inWord = true;
            }
        }
        if (words < 2) {
            return false;
        }
        // TODO: use isWord instead of isDelimiter
        boolean neg = this.count < 0;
        for (int count = Math.max(this.count, -this.count); count > 0; --count) {
            int sta1, end1, sta2, end2;
            // Compute current word boundaries
            sta1 = buf.cursor();
            while (sta1 > lstart && !isDelimiter(buf.atChar(sta1 - 1))) {
                sta1--;
            }
            end1 = sta1;
            while (end1 < lend && !isDelimiter(buf.atChar(++end1)))
                ;
            if (neg) {
                end2 = sta1 - 1;
                while (end2 > lstart && isDelimiter(buf.atChar(end2 - 1))) {
                    end2--;
                }
                if (end2 < lstart) {
                    // No word before, use the word after
                    sta2 = end1;
                    while (isDelimiter(buf.atChar(++sta2)))
                        ;
                    end2 = sta2;
                    while (end2 < lend && !isDelimiter(buf.atChar(++end2)))
                        ;
                } else {
                    sta2 = end2;
                    while (sta2 > lstart && !isDelimiter(buf.atChar(sta2 - 1))) {
                        sta2--;
                    }
                }
            } else {
                sta2 = end1;
                while (sta2 < lend && isDelimiter(buf.atChar(++sta2)))
                    ;
                if (sta2 == lend) {
                    // No word after, use the word before
                    end2 = sta1;
                    while (isDelimiter(buf.atChar(end2 - 1))) {
                        end2--;
                    }
                    sta2 = end2;
                    while (sta2 > lstart && !isDelimiter(buf.atChar(sta2 - 1))) {
                        sta2--;
                    }
                } else {
                    end2 = sta2;
                    while (end2 < lend && !isDelimiter(buf.atChar(++end2)))
                        ;
                }
            }
            if (sta1 < sta2) {
                String res = buf.substring(0, sta1)
                        + buf.substring(sta2, end2)
                        + buf.substring(end1, sta2)
                        + buf.substring(sta1, end1)
                        + buf.substring(end2);
                buf.clear();
                buf.write(res);
                buf.cursor(neg ? end1 : end2);
            } else {
                String res = buf.substring(0, sta2)
                        + buf.substring(sta1, end1)
                        + buf.substring(end2, sta1)
                        + buf.substring(sta2, end2)
                        + buf.substring(end1);
                buf.clear();
                buf.write(res);
                buf.cursor(neg ? end2 : end1);
            }
        }
        return true;
    }

    private int findbol() {
        int x = buf.cursor();
        while (x > 0 && buf.atChar(x - 1) != '\n') {
            x--;
        }
        return x;
    }

    private int findeol() {
        int x = buf.cursor();
        while (x < buf.length() && buf.atChar(x) != '\n') {
            x++;
        }
        return x;
    }

    protected boolean insertComment() {
        return doInsertComment(false);
    }

    protected boolean viInsertComment() {
        return doInsertComment(true);
    }

    protected boolean doInsertComment(boolean isViMode) {
        String comment = getString(COMMENT_BEGIN, DEFAULT_COMMENT_BEGIN);
        beginningOfLine();
        putString(comment);
        if (isViMode) {
            setKeyMap(VIINS);
        }
        return acceptLine();
    }

    protected boolean viFindNextChar() {
        if ((findChar = vigetkey()) > 0) {
            findDir = 1;
            findTailAdd = 0;
            return vifindchar(false);
        }
        return false;
    }

    protected boolean viFindPrevChar() {
        if ((findChar = vigetkey()) > 0) {
            findDir = -1;
            findTailAdd = 0;
            return vifindchar(false);
        }
        return false;
    }

    protected boolean viFindNextCharSkip() {
        if ((findChar = vigetkey()) > 0) {
            findDir = 1;
            findTailAdd = -1;
            return vifindchar(false);
        }
        return false;
    }

    protected boolean viFindPrevCharSkip() {
        if ((findChar = vigetkey()) > 0) {
            findDir = -1;
            findTailAdd = 1;
            return vifindchar(false);
        }
        return false;
    }

    protected boolean viRepeatFind() {
        return vifindchar(true);
    }

    protected boolean viRevRepeatFind() {
        if (count < 0) {
            return callNeg(() -> vifindchar(true));
        }
        findTailAdd = -findTailAdd;
        findDir = -findDir;
        boolean ret = vifindchar(true);
        findTailAdd = -findTailAdd;
        findDir = -findDir;
        return ret;
    }

    private int vigetkey() {
        int ch = readCharacter();
        KeyMap km = keyMaps.get(MAIN);
        if (km != null) {
            Binding b = km.getBound(new String(Character.toChars(ch)));
            if (b instanceof Reference) {
                String func = ((Reference) b).name();
                if (SEND_BREAK.equals(func)) {
                    return -1;
                }
            }
        }
        return ch;
    }

    private boolean vifindchar(boolean repeat) {
        if (findDir == 0) {
            return false;
        }
        if (count < 0) {
            return callNeg(this::viRevRepeatFind);
        }
        if (repeat && findTailAdd != 0) {
            if (findDir > 0) {
                if (buf.cursor() < buf.length() && buf.nextChar() == findChar) {
                    buf.move(1);
                }
            } else {
                if (buf.cursor() > 0 && buf.prevChar() == findChar) {
                    buf.move(-1);
                }
            }
        }
        int cursor = buf.cursor();
        while (count-- > 0) {
            do {
                buf.move(findDir);
            } while (buf.cursor() > 0
                    && buf.cursor() < buf.length()
                    && buf.currChar() != findChar
                    && buf.currChar() != '\n');
            if (buf.cursor() <= 0 || buf.cursor() >= buf.length() || buf.currChar() == '\n') {
                buf.cursor(cursor);
                return false;
            }
        }
        if (findTailAdd != 0) {
            buf.move(findTailAdd);
        }
        if (findDir == 1 && isInViMoveOperation()) {
            buf.move(1);
        }
        return true;
    }

    private boolean callNeg(Widget widget) {
        this.count = -this.count;
        boolean ret = widget.apply();
        this.count = -this.count;
        return ret;
    }

    /**
     * Implements vi search ("/" or "?").
     *
     * @return true if the search was successful
     */
    protected boolean viHistorySearchForward() {
        searchDir = 1;
        searchIndex = 0;
        return getViSearchString() && viRepeatSearch();
    }

    protected boolean viHistorySearchBackward() {
        searchDir = -1;
        searchIndex = history.size() - 1;
        return getViSearchString() && viRepeatSearch();
    }

    protected boolean viRepeatSearch() {
        if (searchDir == 0) {
            return false;
        }
        int si = searchDir < 0
                ? searchBackwards(searchString, searchIndex, false)
                : searchForwards(searchString, searchIndex, false);
        if (si == -1 || si == history.index()) {
            return false;
        }
        searchIndex = si;

        /*
         * Show the match.
         */
        buf.clear();
        history.moveTo(searchIndex);
        buf.write(history.get(searchIndex));
        if (VICMD.equals(keyMap)) {
            buf.move(-1);
        }
        return true;
    }

    protected boolean viRevRepeatSearch() {
        boolean ret;
        searchDir = -searchDir;
        ret = viRepeatSearch();
        searchDir = -searchDir;
        return ret;
    }

    private boolean getViSearchString() {
        if (searchDir == 0) {
            return false;
        }
        String searchPrompt = searchDir < 0 ? "?" : "/";
        Buffer searchBuffer = new BufferImpl();

        KeyMap keyMap = keyMaps.get(MAIN);
        if (keyMap == null) {
            keyMap = keyMaps.get(SAFE);
        }
        while (true) {
            post = () -> new AttributedString(searchPrompt + searchBuffer.toString() + "_");
            redisplay();
            Binding b = doReadBinding(keyMap, null);
            if (b instanceof Reference) {
                String func = ((Reference) b).name();
                switch (func) {
                    case SEND_BREAK:
                        post = null;
                        return false;
                    case ACCEPT_LINE:
                    case VI_CMD_MODE:
                        searchString = searchBuffer.toString();
                        post = null;
                        return true;
                    case MAGIC_SPACE:
                        searchBuffer.write(' ');
                        break;
                    case REDISPLAY:
                        redisplay();
                        break;
                    case CLEAR_SCREEN:
                        clearScreen();
                        break;
                    case SELF_INSERT:
                        searchBuffer.write(getLastBinding());
                        break;
                    case SELF_INSERT_UNMETA:
                        if (getLastBinding().charAt(0) == '\u001b') {
                            String s = getLastBinding().substring(1);
                            if ("\r".equals(s)) {
                                s = "\n";
                            }
                            searchBuffer.write(s);
                        }
                        break;
                    case BACKWARD_DELETE_CHAR:
                    case VI_BACKWARD_DELETE_CHAR:
                        if (searchBuffer.length() > 0) {
                            searchBuffer.backspace();
                        }
                        break;
                    case BACKWARD_KILL_WORD:
                    case VI_BACKWARD_KILL_WORD:
                        if (searchBuffer.length() > 0 && !isWhitespace(searchBuffer.prevChar())) {
                            searchBuffer.backspace();
                        }
                        if (searchBuffer.length() > 0 && isWhitespace(searchBuffer.prevChar())) {
                            searchBuffer.backspace();
                        }
                        break;
                    case QUOTED_INSERT:
                    case VI_QUOTED_INSERT:
                        int c = readCharacter();
                        if (c >= 0) {
                            searchBuffer.write(c);
                        } else {
                            beep();
                        }
                        break;
                    default:
                        beep();
                        break;
                }
            }
        }
    }

    protected boolean insertCloseCurly() {
        return insertClose("}");
    }

    protected boolean insertCloseParen() {
        return insertClose(")");
    }

    protected boolean insertCloseSquare() {
        return insertClose("]");
    }

    protected boolean insertClose(String s) {
        putString(s);

        long blink = getLong(BLINK_MATCHING_PAREN, DEFAULT_BLINK_MATCHING_PAREN);
        if (blink <= 0) {
            removeIndentation();
            return true;
        }

        int closePosition = buf.cursor();

        buf.move(-1);
        doViMatchBracket();
        redisplay();

        peekCharacter(blink);
        int blinkPosition = buf.cursor();
        buf.cursor(closePosition);

        if (blinkPosition != closePosition - 1) {
            removeIndentation();
        }
        return true;
    }

    private void removeIndentation() {
        int indent = getInt(INDENTATION, DEFAULT_INDENTATION);
        if (indent > 0) {
            buf.move(-1);
            for (int i = 0; i < indent; i++) {
                buf.move(-1);
                if (buf.currChar() == ' ') {
                    buf.delete();
                } else {
                    buf.move(1);
                    break;
                }
            }
            buf.move(1);
        }
    }

    protected boolean viMatchBracket() {
        return doViMatchBracket();
    }

    protected boolean undefinedKey() {
        return false;
    }

    /**
     * Implements vi style bracket matching ("%" command). The matching
     * bracket for the current bracket type that you are sitting on is matched.
     *
     * @return true if it worked, false if the cursor was not on a bracket
     *   character or if there was no matching bracket.
     */
    protected boolean doViMatchBracket() {
        int pos = buf.cursor();

        if (pos == buf.length()) {
            return false;
        }

        int type = getBracketType(buf.atChar(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.length()) {
                return false;
            }

            int curType = getBracketType(buf.atChar(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 && isInViMoveOperation()) ++pos;

        buf.cursor(pos);
        return true;
    }

    /**
     * Given a character determines what type of bracket it is (paren,
     * square, curly, or none).
     * @param ch The character to check
     * @return 1 is square, 2 curly, 3 parent, or zero for none.  The value
     *   will be negated if it is the closing form of the bracket.
     */
    protected int getBracketType(int ch) {
        switch (ch) {
            case '[':
                return 1;
            case ']':
                return -1;
            case '{':
                return 2;
            case '}':
                return -2;
            case '(':
                return 3;
            case ')':
                return -3;
            default:
                return 0;
        }
    }

    /**
     * Performs character transpose. The character prior to the cursor and the
     * character under the cursor are swapped and the cursor is advanced one.
     * Do not cross line breaks.
     * @return true
     */
    protected boolean transposeChars() {
        int lstart = buf.cursor() - 1;
        int lend = buf.cursor();
        while (buf.atChar(lstart) != 0 && buf.atChar(lstart) != '\n') {
            lstart--;
        }
        lstart++;
        while (buf.atChar(lend) != 0 && buf.atChar(lend) != '\n') {
            lend++;
        }
        if (lend - lstart < 2) {
            return false;
        }
        boolean neg = this.count < 0;
        for (int count = Math.max(this.count, -this.count); count > 0; --count) {
            while (buf.cursor() <= lstart) {
                buf.move(1);
            }
            while (buf.cursor() >= lend) {
                buf.move(-1);
            }
            int c = buf.currChar();
            buf.currChar(buf.prevChar());
            buf.move(-1);
            buf.currChar(c);
            buf.move(neg ? 0 : 2);
        }
        return true;
    }

    protected boolean undo() {
        isUndo = true;
        if (undo.canUndo()) {
            undo.undo();
            return true;
        }
        return false;
    }

    protected boolean redo() {
        isUndo = true;
        if (undo.canRedo()) {
            undo.redo();
            return true;
        }
        return false;
    }

    protected boolean sendBreak() {
        if (searchTerm == null) {
            buf.clear();
            println();
            redrawLine();
            //            state = State.INTERRUPT;
            return false;
        }
        return true;
    }

    protected boolean backwardChar() {
        return buf.move(-count) != 0;
    }

    protected boolean forwardChar() {
        return buf.move(count) != 0;
    }

    protected boolean viDigitOrBeginningOfLine() {
        if (repeatCount > 0) {
            return digitArgument();
        } else {
            return beginningOfLine();
        }
    }

    protected boolean universalArgument() {
        mult *= universal;
        isArgDigit = true;
        return true;
    }

    protected boolean argumentBase() {
        if (repeatCount > 0 && repeatCount < 32) {
            universal = repeatCount;
            isArgDigit = true;
            return true;
        } else {
            return false;
        }
    }

    protected boolean negArgument() {
        mult *= -1;
        isArgDigit = true;
        return true;
    }

    protected boolean digitArgument() {
        String s = getLastBinding();
        repeatCount = (repeatCount * 10) + s.charAt(s.length() - 1) - '0';
        isArgDigit = true;
        return true;
    }

    protected boolean viDelete() {
        int cursorStart = buf.cursor();
        Binding o = readBinding(getKeys());
        if (o instanceof Reference) {
            // TODO: be smarter on how to get the vi range
            String op = viDeleteChangeYankToRemap(((Reference) o).name());
            // 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 (VI_DELETE.equals(op)) {
                killWholeLine();
            } else {
                viMoveMode = ViMoveMode.DELETE;
                Widget widget = widgets.get(op);
                if (widget != null && !widget.apply()) {
                    viMoveMode = ViMoveMode.NORMAL;
                    return false;
                }
                viMoveMode = ViMoveMode.NORMAL;
            }
            return viDeleteTo(cursorStart, buf.cursor());
        } else {
            pushBackBinding();
            return false;
        }
    }

    protected boolean viYankTo() {
        int cursorStart = buf.cursor();
        Binding o = readBinding(getKeys());
        if (o instanceof Reference) {
            // TODO: be smarter on how to get the vi range
            String op = viDeleteChangeYankToRemap(((Reference) o).name());
            // Similar to delete-to, a "yy" yanks the whole line.
            if (VI_YANK.equals(op)) {
                yankBuffer = buf.toString();
                return true;
            } else {
                viMoveMode = ViMoveMode.YANK;
                Widget widget = widgets.get(op);
                if (widget != null && !widget.apply()) {
                    return false;
                }
                viMoveMode = ViMoveMode.NORMAL;
            }
            return viYankTo(cursorStart, buf.cursor());
        } else {
            pushBackBinding();
            return false;
        }
    }

    protected boolean viYankWholeLine() {
        int s, e;
        int p = buf.cursor();
        while (buf.move(-1) == -1 && buf.prevChar() != '\n')
            ;
        s = buf.cursor();
        for (int i = 0; i < repeatCount; i++) {
            while (buf.move(1) == 1 && buf.prevChar() != '\n')
                ;
        }
        e = buf.cursor();
        yankBuffer = buf.substring(s, e);
        if (!yankBuffer.endsWith("\n")) {
            yankBuffer += "\n";
        }
        buf.cursor(p);
        return true;
    }

    protected boolean viChange() {
        int cursorStart = buf.cursor();
        Binding o = readBinding(getKeys());
        if (o instanceof Reference) {
            // TODO: be smarter on how to get the vi range
            String op = viDeleteChangeYankToRemap(((Reference) o).name());
            // change whole line
            if (VI_CHANGE.equals(op)) {
                killWholeLine();
            } else {
                viMoveMode = ViMoveMode.CHANGE;
                Widget widget = widgets.get(op);
                if (widget != null && !widget.apply()) {
                    viMoveMode = ViMoveMode.NORMAL;
                    return false;
                }
                viMoveMode = ViMoveMode.NORMAL;
            }
            boolean res = viChange(cursorStart, buf.cursor());
            setKeyMap(VIINS);
            return res;
        } else {
            pushBackBinding();
            return false;
        }
    }

    /*
    protected int getViRange(Reference cmd, ViMoveMode mode) {
        Buffer buffer = buf.copy();
        int oldMark = mark;
        int pos = buf.cursor();
        String bind = getLastBinding();

        if (visual != 0) {
            if (buf.length() == 0) {
                return -1;
            }
            pos = mark;
            v
        } else {
            viMoveMode = mode;
            mark = -1;
            Binding b = doReadBinding(getKeys(), keyMaps.get(VIOPP));
            if (b == null || new Reference(SEND_BREAK).equals(b)) {
                viMoveMode = ViMoveMode.NORMAL;
                mark = oldMark;
                return -1;
            }
            if (cmd.equals(b)) {
                doViLineRange();
            }
            Widget w = getWidget(b);
            if (w )
            if (b instanceof Reference) {

            }
        }

    }
    */

    protected void cleanup() {
        if (isSet(Option.ERASE_LINE_ON_FINISH)) {
            Buffer oldBuffer = buf.copy();
            AttributedString oldPrompt = prompt;
            buf.clear();
            prompt = new AttributedString("");
            doCleanup(false);
            prompt = oldPrompt;
            buf.copyFrom(oldBuffer);
        } else {
            doCleanup(true);
        }
    }

    protected void doCleanup(boolean nl) {
        buf.cursor(buf.length());
        post = null;
        if (size.getColumns() > 0 || size.getRows() > 0) {
            doAutosuggestion = false;
            redisplay(false);
            if (nl) {
                println();
            }
            terminal.puts(Capability.keypad_local);
            terminal.trackMouse(Terminal.MouseTracking.Off);
            if (isSet(Option.BRACKETED_PASTE) && !isTerminalDumb())
                terminal.writer().write(BRACKETED_PASTE_OFF);
            flush();
        }
        history.moveToEnd();
    }

    protected boolean historyIncrementalSearchForward() {
        return doSearchHistory(false);
    }

    protected boolean historyIncrementalSearchBackward() {
        return doSearchHistory(true);
    }

    static class Pair {
        final U u;
        final V v;

        public Pair(U u, V v) {
            this.u = u;
            this.v = v;
        }

        public U getU() {
            return u;
        }

        public V getV() {
            return v;
        }
    }

    protected boolean doSearchHistory(boolean backward) {
        if (history.isEmpty()) {
            return false;
        }

        KeyMap terminators = new KeyMap<>();
        getString(SEARCH_TERMINATORS, DEFAULT_SEARCH_TERMINATORS)
                .codePoints()
                .forEach(c -> bind(terminators, ACCEPT_LINE, new String(Character.toChars(c))));

        Buffer originalBuffer = buf.copy();
        searchIndex = -1;
        searchTerm = new StringBuffer();
        searchBackward = backward;
        searchFailing = false;
        post = () -> new AttributedString((searchFailing ? "failing" + " " : "")
                + (searchBackward ? "bck-i-search" : "fwd-i-search")
                + ": " + searchTerm + "_");

        redisplay();
        try {
            while (true) {
                int prevSearchIndex = searchIndex;
                Binding operation = readBinding(getKeys(), terminators);
                String ref = (operation instanceof Reference) ? ((Reference) operation).name() : "";
                boolean next = false;
                switch (ref) {
                    case SEND_BREAK:
                        beep();
                        buf.copyFrom(originalBuffer);
                        return true;
                    case HISTORY_INCREMENTAL_SEARCH_BACKWARD:
                        searchBackward = true;
                        next = true;
                        break;
                    case HISTORY_INCREMENTAL_SEARCH_FORWARD:
                        searchBackward = false;
                        next = true;
                        break;
                    case BACKWARD_DELETE_CHAR:
                        if (searchTerm.length() > 0) {
                            searchTerm.deleteCharAt(searchTerm.length() - 1);
                        }
                        break;
                    case SELF_INSERT:
                        searchTerm.append(getLastBinding());
                        break;
                    default:
                        // Set buffer and cursor position to the found string.
                        if (searchIndex != -1) {
                            history.moveTo(searchIndex);
                        }
                        pushBackBinding();
                        return true;
                }

                // print the search status
                String pattern = doGetSearchPattern();
                if (pattern.length() == 0) {
                    buf.copyFrom(originalBuffer);
                    searchFailing = false;
                } else {
                    boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH);
                    Pattern pat = Pattern.compile(
                            pattern,
                            caseInsensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE : Pattern.UNICODE_CASE);
                    Pair pair = null;
                    if (searchBackward) {
                        boolean nextOnly = next;
                        pair = matches(pat, buf.toString(), searchIndex).stream()
                                .filter(p -> nextOnly ? p.v < buf.cursor() : p.v <= buf.cursor())
                                .max(Comparator.comparing(Pair::getV))
                                .orElse(null);
                        if (pair == null) {
                            pair = StreamSupport.stream(
                                            Spliterators.spliteratorUnknownSize(
                                                    history.reverseIterator(
                                                            searchIndex < 0 ? history.last() : searchIndex - 1),
                                                    Spliterator.ORDERED),
                                            false)
                                    .flatMap(e -> matches(pat, e.line(), e.index()).stream())
                                    .findFirst()
                                    .orElse(null);
                        }
                    } else {
                        boolean nextOnly = next;
                        pair = matches(pat, buf.toString(), searchIndex).stream()
                                .filter(p -> nextOnly ? p.v > buf.cursor() : p.v >= buf.cursor())
                                .min(Comparator.comparing(Pair::getV))
                                .orElse(null);
                        if (pair == null) {
                            pair = StreamSupport.stream(
                                            Spliterators.spliteratorUnknownSize(
                                                    history.iterator(
                                                            (searchIndex < 0 ? history.last() : searchIndex) + 1),
                                                    Spliterator.ORDERED),
                                            false)
                                    .flatMap(e -> matches(pat, e.line(), e.index()).stream())
                                    .findFirst()
                                    .orElse(null);
                            if (pair == null && searchIndex >= 0) {
                                pair = matches(pat, originalBuffer.toString(), -1).stream()
                                        .min(Comparator.comparing(Pair::getV))
                                        .orElse(null);
                            }
                        }
                    }
                    if (pair != null) {
                        searchIndex = pair.u;
                        buf.clear();
                        if (searchIndex >= 0) {
                            buf.write(history.get(searchIndex));
                        } else {
                            buf.write(originalBuffer.toString());
                        }
                        buf.cursor(pair.v);
                        searchFailing = false;
                    } else {
                        searchFailing = true;
                        beep();
                    }
                }
                redisplay();
            }
        } catch (IOError e) {
            // Ignore Ctrl+C interrupts and just exit the loop
            if (!(e.getCause() instanceof InterruptedException)) {
                throw e;
            }
            return true;
        } finally {
            searchTerm = null;
            searchIndex = -1;
            post = null;
        }
    }

    private List> matches(Pattern p, String line, int index) {
        List> starts = new ArrayList<>();
        Matcher m = p.matcher(line);
        while (m.find()) {
            starts.add(new Pair<>(index, m.start()));
        }
        return starts;
    }

    private String doGetSearchPattern() {
        StringBuilder sb = new StringBuilder();
        boolean inQuote = false;
        for (int i = 0; i < searchTerm.length(); i++) {
            char c = searchTerm.charAt(i);
            if (Character.isLowerCase(c)) {
                if (inQuote) {
                    sb.append("\\E");
                    inQuote = false;
                }
                sb.append("[")
                        .append(Character.toLowerCase(c))
                        .append(Character.toUpperCase(c))
                        .append("]");
            } else {
                if (!inQuote) {
                    sb.append("\\Q");
                    inQuote = true;
                }
                sb.append(c);
            }
        }
        if (inQuote) {
            sb.append("\\E");
        }
        return sb.toString();
    }

    private void pushBackBinding() {
        pushBackBinding(false);
    }

    private void pushBackBinding(boolean skip) {
        String s = getLastBinding();
        if (s != null) {
            bindingReader.runMacro(s);
            skipRedisplay = skip;
        }
    }

    protected boolean historySearchForward() {
        if (historyBuffer == null || buf.length() == 0 || !buf.toString().equals(history.current())) {
            historyBuffer = buf.copy();
            searchBuffer = getFirstWord();
        }
        int index = history.index() + 1;

        if (index < history.last() + 1) {
            int searchIndex = searchForwards(searchBuffer.toString(), index, true);
            if (searchIndex == -1) {
                history.moveToEnd();
                if (!buf.toString().equals(historyBuffer.toString())) {
                    setBuffer(historyBuffer.toString());
                    historyBuffer = null;
                } else {
                    return false;
                }
            } else {
                // Maintain cursor position while searching.
                if (history.moveTo(searchIndex)) {
                    setBuffer(history.current());
                } else {
                    history.moveToEnd();
                    setBuffer(historyBuffer.toString());
                    return false;
                }
            }
        } else {
            history.moveToEnd();
            if (!buf.toString().equals(historyBuffer.toString())) {
                setBuffer(historyBuffer.toString());
                historyBuffer = null;
            } else {
                return false;
            }
        }
        return true;
    }

    private CharSequence getFirstWord() {
        String s = buf.toString();
        int i = 0;
        while (i < s.length() && !Character.isWhitespace(s.charAt(i))) {
            i++;
        }
        return s.substring(0, i);
    }

    protected boolean historySearchBackward() {
        if (historyBuffer == null || buf.length() == 0 || !buf.toString().equals(history.current())) {
            historyBuffer = buf.copy();
            searchBuffer = getFirstWord();
        }
        int searchIndex = searchBackwards(searchBuffer.toString(), history.index(), true);

        if (searchIndex == -1) {
            return false;
        } else {
            // Maintain cursor position while searching.
            if (history.moveTo(searchIndex)) {
                setBuffer(history.current());
            } else {
                return false;
            }
        }
        return true;
    }

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

    public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) {
        boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH);
        if (caseInsensitive) {
            searchTerm = searchTerm.toLowerCase();
        }
        ListIterator it = history.iterator(startIndex);
        while (it.hasPrevious()) {
            History.Entry e = it.previous();
            String line = e.line();
            if (caseInsensitive) {
                line = line.toLowerCase();
            }
            int idx = line.indexOf(searchTerm);
            if ((startsWith && idx == 0) || (!startsWith && idx >= 0)) {
                return e.index();
            }
        }
        return -1;
    }

    public int searchForwards(String searchTerm, int startIndex, boolean startsWith) {
        boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH);
        if (caseInsensitive) {
            searchTerm = searchTerm.toLowerCase();
        }
        if (startIndex > history.last()) {
            startIndex = history.last();
        }
        ListIterator it = history.iterator(startIndex);
        if (searchIndex != -1 && it.hasNext()) {
            it.next();
        }
        while (it.hasNext()) {
            History.Entry e = it.next();
            String line = e.line();
            if (caseInsensitive) {
                line = line.toLowerCase();
            }
            int idx = line.indexOf(searchTerm);
            if ((startsWith && idx == 0) || (!startsWith && idx >= 0)) {
                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());
    }

    protected boolean quit() {
        getBuffer().clear();
        return acceptLine();
    }

    protected boolean acceptAndHold() {
        nextCommandFromHistory = false;
        acceptLine();
        if (!buf.toString().isEmpty()) {
            nextHistoryId = Integer.MAX_VALUE;
            nextCommandFromHistory = true;
        }
        return nextCommandFromHistory;
    }

    protected boolean acceptLineAndDownHistory() {
        nextCommandFromHistory = false;
        acceptLine();
        if (nextHistoryId < 0) {
            nextHistoryId = history.index();
        }
        if (history.size() > nextHistoryId + 1) {
            nextHistoryId++;
            nextCommandFromHistory = true;
        }
        return nextCommandFromHistory;
    }

    protected boolean acceptAndInferNextHistory() {
        nextCommandFromHistory = false;
        acceptLine();
        if (!buf.toString().isEmpty()) {
            nextHistoryId = searchBackwards(buf.toString(), history.last());
            if (nextHistoryId >= 0 && history.size() > nextHistoryId + 1) {
                nextHistoryId++;
                nextCommandFromHistory = true;
            }
        }
        return nextCommandFromHistory;
    }

    protected boolean acceptLine() {
        parsedLine = null;
        int curPos = 0;
        if (!isSet(Option.DISABLE_EVENT_EXPANSION)) {
            try {
                String str = buf.toString();
                String exp = expander.expandHistory(history, str);
                if (!exp.equals(str)) {
                    buf.clear();
                    buf.write(exp);
                    if (isSet(Option.HISTORY_VERIFY)) {
                        return true;
                    }
                }
            } catch (IllegalArgumentException e) {
                // Ignore
            }
        }
        try {
            curPos = buf.cursor();
            parsedLine = parser.parse(buf.toString(), buf.cursor(), ParseContext.ACCEPT_LINE);
        } catch (EOFError e) {
            StringBuilder sb = new StringBuilder("\n");
            indention(e.getOpenBrackets(), sb);
            int curMove = sb.length();
            if (isSet(Option.INSERT_BRACKET) && e.getOpenBrackets() > 1 && e.getNextClosingBracket() != null) {
                sb.append('\n');
                indention(e.getOpenBrackets() - 1, sb);
                sb.append(e.getNextClosingBracket());
            }
            buf.write(sb.toString());
            buf.cursor(curPos + curMove);
            return true;
        } catch (SyntaxError e) {
            // do nothing
        }
        callWidget(CALLBACK_FINISH);
        state = State.DONE;
        return true;
    }

    void indention(int nb, StringBuilder sb) {
        int indent = getInt(INDENTATION, DEFAULT_INDENTATION) * nb;
        for (int i = 0; i < indent; i++) {
            sb.append(' ');
        }
    }

    protected boolean selfInsert() {
        for (int count = this.count; count > 0; count--) {
            putString(getLastBinding());
        }
        return true;
    }

    protected boolean selfInsertUnmeta() {
        if (getLastBinding().charAt(0) == '\u001b') {
            String s = getLastBinding().substring(1);
            if ("\r".equals(s)) {
                s = "\n";
            }
            for (int count = this.count; count > 0; count--) {
                putString(s);
            }
            return true;
        } else {
            return false;
        }
    }

    protected boolean overwriteMode() {
        overTyping = !overTyping;
        return true;
    }

    //
    // History Control
    //

    protected boolean beginningOfBufferOrHistory() {
        if (findbol() != 0) {
            buf.cursor(0);
            return true;
        } else {
            return beginningOfHistory();
        }
    }

    protected boolean beginningOfHistory() {
        if (history.moveToFirst()) {
            setBuffer(history.current());
            return true;
        } else {
            return false;
        }
    }

    protected boolean endOfBufferOrHistory() {
        if (findeol() != buf.length()) {
            buf.cursor(buf.length());
            return true;
        } else {
            return endOfHistory();
        }
    }

    protected boolean endOfHistory() {
        if (history.moveToLast()) {
            setBuffer(history.current());
            return true;
        } else {
            return false;
        }
    }

    protected boolean beginningOfLineHist() {
        if (count < 0) {
            return callNeg(this::endOfLineHist);
        }
        while (count-- > 0) {
            int bol = findbol();
            if (bol != buf.cursor()) {
                buf.cursor(bol);
            } else {
                moveHistory(false);
                buf.cursor(0);
            }
        }
        return true;
    }

    protected boolean endOfLineHist() {
        if (count < 0) {
            return callNeg(this::beginningOfLineHist);
        }
        while (count-- > 0) {
            int eol = findeol();
            if (eol != buf.cursor()) {
                buf.cursor(eol);
            } else {
                moveHistory(true);
            }
        }
        return true;
    }

    protected boolean upHistory() {
        while (count-- > 0) {
            if (!moveHistory(false)) {
                return !isSet(Option.HISTORY_BEEP);
            }
        }
        return true;
    }

    protected boolean downHistory() {
        while (count-- > 0) {
            if (!moveHistory(true)) {
                return !isSet(Option.HISTORY_BEEP);
            }
        }
        return true;
    }

    protected boolean viUpLineOrHistory() {
        return upLine() || upHistory() && viFirstNonBlank();
    }

    protected boolean viDownLineOrHistory() {
        return downLine() || downHistory() && viFirstNonBlank();
    }

    protected boolean upLine() {
        return buf.up();
    }

    protected boolean downLine() {
        return buf.down();
    }

    protected boolean upLineOrHistory() {
        return upLine() || upHistory();
    }

    protected boolean upLineOrSearch() {
        return upLine() || historySearchBackward();
    }

    protected boolean downLineOrHistory() {
        return downLine() || downHistory();
    }

    protected boolean downLineOrSearch() {
        return downLine() || historySearchForward();
    }

    protected boolean viCmdMode() {
        // 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 explicit entry to movement
        // mode.
        if (state == State.NORMAL) {
            buf.move(-1);
        }
        return setKeyMap(VICMD);
    }

    protected boolean viInsert() {
        return setKeyMap(VIINS);
    }

    protected boolean viAddNext() {
        buf.move(1);
        return setKeyMap(VIINS);
    }

    protected boolean viAddEol() {
        return endOfLine() && setKeyMap(VIINS);
    }

    protected boolean emacsEditingMode() {
        return setKeyMap(EMACS);
    }

    protected boolean viChangeWholeLine() {
        return viFirstNonBlank() && viChangeEol();
    }

    protected boolean viChangeEol() {
        return viChange(buf.cursor(), buf.length()) && setKeyMap(VIINS);
    }

    protected boolean viKillEol() {
        int eol = findeol();
        if (buf.cursor() == eol) {
            return false;
        }
        killRing.add(buf.substring(buf.cursor(), eol));
        buf.delete(eol - buf.cursor());
        return true;
    }

    protected boolean quotedInsert() {
        int c = readCharacter();
        while (count-- > 0) {
            putString(new String(Character.toChars(c)));
        }
        return true;
    }

    protected boolean viJoin() {
        if (buf.down()) {
            while (buf.move(-1) == -1 && buf.prevChar() != '\n')
                ;
            buf.backspace();
            buf.write(' ');
            buf.move(-1);
            return true;
        }
        return false;
    }

    protected boolean viKillWholeLine() {
        return killWholeLine() && setKeyMap(VIINS);
    }

    protected boolean viInsertBol() {
        return beginningOfLine() && setKeyMap(VIINS);
    }

    protected boolean backwardDeleteChar() {
        if (count < 0) {
            return callNeg(this::deleteChar);
        }
        if (buf.cursor() == 0) {
            return false;
        }
        buf.backspace(count);
        return true;
    }

    protected boolean viFirstNonBlank() {
        beginningOfLine();
        while (buf.cursor() < buf.length() && isWhitespace(buf.currChar())) {
            buf.move(1);
        }
        return true;
    }

    protected boolean viBeginningOfLine() {
        buf.cursor(findbol());
        return true;
    }

    protected boolean viEndOfLine() {
        if (count < 0) {
            return false;
        }
        while (count-- > 0) {
            buf.cursor(findeol() + 1);
        }
        buf.move(-1);
        return true;
    }

    protected boolean beginningOfLine() {
        while (count-- > 0) {
            while (buf.move(-1) == -1 && buf.prevChar() != '\n')
                ;
        }
        return true;
    }

    protected boolean endOfLine() {
        while (count-- > 0) {
            while (buf.move(1) == 1 && buf.currChar() != '\n')
                ;
        }
        return true;
    }

    protected boolean deleteChar() {
        if (count < 0) {
            return callNeg(this::backwardDeleteChar);
        }
        if (buf.cursor() == buf.length()) {
            return false;
        }
        buf.delete(count);
        return true;
    }

    /**
     * Deletes the previous character from the cursor position
     * @return true if it succeeded, false otherwise
     */
    protected boolean viBackwardDeleteChar() {
        for (int i = 0; i < count; i++) {
            if (!buf.backspace()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Deletes the character you are sitting on and sucks the rest of
     * the line in from the right.
     * @return true if it succeeded, false otherwise
     */
    protected boolean viDeleteChar() {
        for (int i = 0; i < count; i++) {
            if (!buf.delete()) {
                return false;
            }
        }
        return true;
    }

    /**
     * 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.
     * @return true if it succeeded, false otherwise
     */
    protected boolean viSwapCase() {
        for (int i = 0; i < count; i++) {
            if (buf.cursor() < buf.length()) {
                int ch = buf.atChar(buf.cursor());
                ch = switchCase(ch);
                buf.currChar(ch);
                buf.move(1);
            } else {
                return false;
            }
        }
        return true;
    }

    /**
     * Implements the vi change character command (in move-mode "r"
     * followed by the character to change to).
     * @return true if it succeeded, false otherwise
     */
    protected boolean viReplaceChars() {
        int c = readCharacter();
        // EOF, ESC, or CTRL-C aborts.
        if (c < 0 || c == '\033' || c == '\003') {
            return true;
        }

        for (int i = 0; i < count; i++) {
            if (buf.currChar((char) c)) {
                if (i < count - 1) {
                    buf.move(1);
                }
            } else {
                return false;
            }
        }
        return true;
    }

    protected boolean viChange(int startPos, int endPos) {
        return doViDeleteOrChange(startPos, endPos, true);
    }

    protected boolean viDeleteTo(int startPos, int endPos) {
        return doViDeleteOrChange(startPos, endPos, false);
    }

    /**
     * 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
     */
    protected boolean doViDeleteOrChange(int startPos, int endPos, boolean isChange) {
        if (startPos == endPos) {
            return true;
        }

        if (endPos < startPos) {
            int tmp = endPos;
            endPos = startPos;
            startPos = tmp;
        }

        buf.cursor(startPos);
        buf.delete(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()) {
            buf.move(-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 example "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
     */
    protected boolean viYankTo(int startPos, int endPos) {
        int cursorPos = startPos;

        if (endPos < startPos) {
            int tmp = endPos;
            endPos = startPos;
            startPos = tmp;
        }

        if (startPos == endPos) {
            yankBuffer = "";
            return true;
        }

        yankBuffer = buf.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.
         */
        buf.cursor(cursorPos);
        return true;
    }

    protected boolean viOpenLineAbove() {
        while (buf.move(-1) == -1 && buf.prevChar() != '\n')
            ;
        buf.write('\n');
        buf.move(-1);
        return setKeyMap(VIINS);
    }

    protected boolean viOpenLineBelow() {
        while (buf.move(1) == 1 && buf.currChar() != '\n')
            ;
        buf.write('\n');
        return setKeyMap(VIINS);
    }

    /**
     * Pasts the yank buffer to the right of the current cursor position
     * and moves the cursor to the end of the pasted region.
     * @return true
     */
    protected boolean viPutAfter() {
        if (yankBuffer.indexOf('\n') >= 0) {
            while (buf.move(1) == 1 && buf.currChar() != '\n')
                ;
            buf.move(1);
            putString(yankBuffer);
            buf.move(-yankBuffer.length());
        } else if (yankBuffer.length() != 0) {
            if (buf.cursor() < buf.length()) {
                buf.move(1);
            }
            for (int i = 0; i < count; i++) {
                putString(yankBuffer);
            }
            buf.move(-1);
        }
        return true;
    }

    protected boolean viPutBefore() {
        if (yankBuffer.indexOf('\n') >= 0) {
            while (buf.move(-1) == -1 && buf.prevChar() != '\n')
                ;
            putString(yankBuffer);
            buf.move(-yankBuffer.length());
        } else if (yankBuffer.length() != 0) {
            if (buf.cursor() > 0) {
                buf.move(-1);
            }
            for (int i = 0; i < count; i++) {
                putString(yankBuffer);
            }
            buf.move(-1);
        }
        return true;
    }

    protected boolean doLowercaseVersion() {
        bindingReader.runMacro(getLastBinding().toLowerCase());
        return true;
    }

    protected boolean setMarkCommand() {
        if (count < 0) {
            regionActive = RegionType.NONE;
            return true;
        }
        regionMark = buf.cursor();
        regionActive = RegionType.CHAR;
        return true;
    }

    protected boolean exchangePointAndMark() {
        if (count == 0) {
            regionActive = RegionType.CHAR;
            return true;
        }
        int x = regionMark;
        regionMark = buf.cursor();
        buf.cursor(x);
        if (buf.cursor() > buf.length()) {
            buf.cursor(buf.length());
        }
        if (count > 0) {
            regionActive = RegionType.CHAR;
        }
        return true;
    }

    protected boolean visualMode() {
        if (isInViMoveOperation()) {
            isArgDigit = true;
            forceLine = false;
            forceChar = true;
            return true;
        }
        if (regionActive == RegionType.NONE) {
            regionMark = buf.cursor();
            regionActive = RegionType.CHAR;
        } else if (regionActive == RegionType.CHAR) {
            regionActive = RegionType.NONE;
        } else if (regionActive == RegionType.LINE) {
            regionActive = RegionType.CHAR;
        }
        return true;
    }

    protected boolean visualLineMode() {
        if (isInViMoveOperation()) {
            isArgDigit = true;
            forceLine = true;
            forceChar = false;
            return true;
        }
        if (regionActive == RegionType.NONE) {
            regionMark = buf.cursor();
            regionActive = RegionType.LINE;
        } else if (regionActive == RegionType.CHAR) {
            regionActive = RegionType.LINE;
        } else if (regionActive == RegionType.LINE) {
            regionActive = RegionType.NONE;
        }
        return true;
    }

    protected boolean deactivateRegion() {
        regionActive = RegionType.NONE;
        return true;
    }

    protected boolean whatCursorPosition() {
        post = () -> {
            AttributedStringBuilder sb = new AttributedStringBuilder();
            if (buf.cursor() < buf.length()) {
                int c = buf.currChar();
                sb.append("Char: ");
                if (c == ' ') {
                    sb.append("SPC");
                } else if (c == '\n') {
                    sb.append("LFD");
                } else if (c < 32) {
                    sb.append('^');
                    sb.append((char) (c + 'A' - 1));
                } else if (c == 127) {
                    sb.append("^?");
                } else {
                    sb.append((char) c);
                }
                sb.append(" (");
                sb.append("0").append(Integer.toOctalString(c)).append(" ");
                sb.append(Integer.toString(c)).append(" ");
                sb.append("0x").append(Integer.toHexString(c)).append(" ");
                sb.append(")");
            } else {
                sb.append("EOF");
            }
            sb.append("   ");
            sb.append("point ");
            sb.append(Integer.toString(buf.cursor() + 1));
            sb.append(" of ");
            sb.append(Integer.toString(buf.length() + 1));
            sb.append(" (");
            sb.append(Integer.toString(buf.length() == 0 ? 100 : ((100 * buf.cursor()) / buf.length())));
            sb.append("%)");
            sb.append("   ");
            sb.append("column ");
            sb.append(Integer.toString(buf.cursor() - findbol()));
            return sb.toAttributedString();
        };
        return true;
    }

    protected boolean editAndExecute() {
        boolean out = true;
        File file = null;
        try {
            file = File.createTempFile("jline-execute-", null);
            try (FileWriter writer = new FileWriter(file)) {
                writer.write(buf.toString());
            }
            editAndAddInBuffer(file);
        } catch (Exception e) {
            e.printStackTrace(terminal.writer());
            out = false;
        } finally {
            state = State.IGNORE;
            if (file != null && file.exists()) {
                file.delete();
            }
        }
        return out;
    }

    protected Map builtinWidgets() {
        Map widgets = new HashMap<>();
        addBuiltinWidget(widgets, ACCEPT_AND_INFER_NEXT_HISTORY, this::acceptAndInferNextHistory);
        addBuiltinWidget(widgets, ACCEPT_AND_HOLD, this::acceptAndHold);
        addBuiltinWidget(widgets, ACCEPT_LINE, this::acceptLine);
        addBuiltinWidget(widgets, ACCEPT_LINE_AND_DOWN_HISTORY, this::acceptLineAndDownHistory);
        addBuiltinWidget(widgets, ARGUMENT_BASE, this::argumentBase);
        addBuiltinWidget(widgets, BACKWARD_CHAR, this::backwardChar);
        addBuiltinWidget(widgets, BACKWARD_DELETE_CHAR, this::backwardDeleteChar);
        addBuiltinWidget(widgets, BACKWARD_DELETE_WORD, this::backwardDeleteWord);
        addBuiltinWidget(widgets, BACKWARD_KILL_LINE, this::backwardKillLine);
        addBuiltinWidget(widgets, BACKWARD_KILL_WORD, this::backwardKillWord);
        addBuiltinWidget(widgets, BACKWARD_WORD, this::backwardWord);
        addBuiltinWidget(widgets, BEEP, this::beep);
        addBuiltinWidget(widgets, BEGINNING_OF_BUFFER_OR_HISTORY, this::beginningOfBufferOrHistory);
        addBuiltinWidget(widgets, BEGINNING_OF_HISTORY, this::beginningOfHistory);
        addBuiltinWidget(widgets, BEGINNING_OF_LINE, this::beginningOfLine);
        addBuiltinWidget(widgets, BEGINNING_OF_LINE_HIST, this::beginningOfLineHist);
        addBuiltinWidget(widgets, CAPITALIZE_WORD, this::capitalizeWord);
        addBuiltinWidget(widgets, CLEAR, this::clear);
        addBuiltinWidget(widgets, CLEAR_SCREEN, this::clearScreen);
        addBuiltinWidget(widgets, COMPLETE_PREFIX, this::completePrefix);
        addBuiltinWidget(widgets, COMPLETE_WORD, this::completeWord);
        addBuiltinWidget(widgets, COPY_PREV_WORD, this::copyPrevWord);
        addBuiltinWidget(widgets, COPY_REGION_AS_KILL, this::copyRegionAsKill);
        addBuiltinWidget(widgets, DELETE_CHAR, this::deleteChar);
        addBuiltinWidget(widgets, DELETE_CHAR_OR_LIST, this::deleteCharOrList);
        addBuiltinWidget(widgets, DELETE_WORD, this::deleteWord);
        addBuiltinWidget(widgets, DIGIT_ARGUMENT, this::digitArgument);
        addBuiltinWidget(widgets, DO_LOWERCASE_VERSION, this::doLowercaseVersion);
        addBuiltinWidget(widgets, DOWN_CASE_WORD, this::downCaseWord);
        addBuiltinWidget(widgets, DOWN_LINE, this::downLine);
        addBuiltinWidget(widgets, DOWN_LINE_OR_HISTORY, this::downLineOrHistory);
        addBuiltinWidget(widgets, DOWN_LINE_OR_SEARCH, this::downLineOrSearch);
        addBuiltinWidget(widgets, DOWN_HISTORY, this::downHistory);
        addBuiltinWidget(widgets, EDIT_AND_EXECUTE_COMMAND, this::editAndExecute);
        addBuiltinWidget(widgets, EMACS_EDITING_MODE, this::emacsEditingMode);
        addBuiltinWidget(widgets, EMACS_BACKWARD_WORD, this::emacsBackwardWord);
        addBuiltinWidget(widgets, EMACS_FORWARD_WORD, this::emacsForwardWord);
        addBuiltinWidget(widgets, END_OF_BUFFER_OR_HISTORY, this::endOfBufferOrHistory);
        addBuiltinWidget(widgets, END_OF_HISTORY, this::endOfHistory);
        addBuiltinWidget(widgets, END_OF_LINE, this::endOfLine);
        addBuiltinWidget(widgets, END_OF_LINE_HIST, this::endOfLineHist);
        addBuiltinWidget(widgets, EXCHANGE_POINT_AND_MARK, this::exchangePointAndMark);
        addBuiltinWidget(widgets, EXPAND_HISTORY, this::expandHistory);
        addBuiltinWidget(widgets, EXPAND_OR_COMPLETE, this::expandOrComplete);
        addBuiltinWidget(widgets, EXPAND_OR_COMPLETE_PREFIX, this::expandOrCompletePrefix);
        addBuiltinWidget(widgets, EXPAND_WORD, this::expandWord);
        addBuiltinWidget(widgets, FRESH_LINE, this::freshLine);
        addBuiltinWidget(widgets, FORWARD_CHAR, this::forwardChar);
        addBuiltinWidget(widgets, FORWARD_WORD, this::forwardWord);
        addBuiltinWidget(widgets, HISTORY_INCREMENTAL_SEARCH_BACKWARD, this::historyIncrementalSearchBackward);
        addBuiltinWidget(widgets, HISTORY_INCREMENTAL_SEARCH_FORWARD, this::historyIncrementalSearchForward);
        addBuiltinWidget(widgets, HISTORY_SEARCH_BACKWARD, this::historySearchBackward);
        addBuiltinWidget(widgets, HISTORY_SEARCH_FORWARD, this::historySearchForward);
        addBuiltinWidget(widgets, INSERT_CLOSE_CURLY, this::insertCloseCurly);
        addBuiltinWidget(widgets, INSERT_CLOSE_PAREN, this::insertCloseParen);
        addBuiltinWidget(widgets, INSERT_CLOSE_SQUARE, this::insertCloseSquare);
        addBuiltinWidget(widgets, INSERT_COMMENT, this::insertComment);
        addBuiltinWidget(widgets, KILL_BUFFER, this::killBuffer);
        addBuiltinWidget(widgets, KILL_LINE, this::killLine);
        addBuiltinWidget(widgets, KILL_REGION, this::killRegion);
        addBuiltinWidget(widgets, KILL_WHOLE_LINE, this::killWholeLine);
        addBuiltinWidget(widgets, KILL_WORD, this::killWord);
        addBuiltinWidget(widgets, LIST_CHOICES, this::listChoices);
        addBuiltinWidget(widgets, MENU_COMPLETE, this::menuComplete);
        addBuiltinWidget(widgets, MENU_EXPAND_OR_COMPLETE, this::menuExpandOrComplete);
        addBuiltinWidget(widgets, NEG_ARGUMENT, this::negArgument);
        addBuiltinWidget(widgets, OVERWRITE_MODE, this::overwriteMode);
        //        addBuiltinWidget(widgets, QUIT, this::quit);
        addBuiltinWidget(widgets, QUOTED_INSERT, this::quotedInsert);
        addBuiltinWidget(widgets, REDISPLAY, this::redisplay);
        addBuiltinWidget(widgets, REDRAW_LINE, this::redrawLine);
        addBuiltinWidget(widgets, REDO, this::redo);
        addBuiltinWidget(widgets, SELF_INSERT, this::selfInsert);
        addBuiltinWidget(widgets, SELF_INSERT_UNMETA, this::selfInsertUnmeta);
        addBuiltinWidget(widgets, SEND_BREAK, this::sendBreak);
        addBuiltinWidget(widgets, SET_MARK_COMMAND, this::setMarkCommand);
        addBuiltinWidget(widgets, TRANSPOSE_CHARS, this::transposeChars);
        addBuiltinWidget(widgets, TRANSPOSE_WORDS, this::transposeWords);
        addBuiltinWidget(widgets, UNDEFINED_KEY, this::undefinedKey);
        addBuiltinWidget(widgets, UNIVERSAL_ARGUMENT, this::universalArgument);
        addBuiltinWidget(widgets, UNDO, this::undo);
        addBuiltinWidget(widgets, UP_CASE_WORD, this::upCaseWord);
        addBuiltinWidget(widgets, UP_HISTORY, this::upHistory);
        addBuiltinWidget(widgets, UP_LINE, this::upLine);
        addBuiltinWidget(widgets, UP_LINE_OR_HISTORY, this::upLineOrHistory);
        addBuiltinWidget(widgets, UP_LINE_OR_SEARCH, this::upLineOrSearch);
        addBuiltinWidget(widgets, VI_ADD_EOL, this::viAddEol);
        addBuiltinWidget(widgets, VI_ADD_NEXT, this::viAddNext);
        addBuiltinWidget(widgets, VI_BACKWARD_CHAR, this::viBackwardChar);
        addBuiltinWidget(widgets, VI_BACKWARD_DELETE_CHAR, this::viBackwardDeleteChar);
        addBuiltinWidget(widgets, VI_BACKWARD_BLANK_WORD, this::viBackwardBlankWord);
        addBuiltinWidget(widgets, VI_BACKWARD_BLANK_WORD_END, this::viBackwardBlankWordEnd);
        addBuiltinWidget(widgets, VI_BACKWARD_KILL_WORD, this::viBackwardKillWord);
        addBuiltinWidget(widgets, VI_BACKWARD_WORD, this::viBackwardWord);
        addBuiltinWidget(widgets, VI_BACKWARD_WORD_END, this::viBackwardWordEnd);
        addBuiltinWidget(widgets, VI_BEGINNING_OF_LINE, this::viBeginningOfLine);
        addBuiltinWidget(widgets, VI_CMD_MODE, this::viCmdMode);
        addBuiltinWidget(widgets, VI_DIGIT_OR_BEGINNING_OF_LINE, this::viDigitOrBeginningOfLine);
        addBuiltinWidget(widgets, VI_DOWN_LINE_OR_HISTORY, this::viDownLineOrHistory);
        addBuiltinWidget(widgets, VI_CHANGE, this::viChange);
        addBuiltinWidget(widgets, VI_CHANGE_EOL, this::viChangeEol);
        addBuiltinWidget(widgets, VI_CHANGE_WHOLE_LINE, this::viChangeWholeLine);
        addBuiltinWidget(widgets, VI_DELETE_CHAR, this::viDeleteChar);
        addBuiltinWidget(widgets, VI_DELETE, this::viDelete);
        addBuiltinWidget(widgets, VI_END_OF_LINE, this::viEndOfLine);
        addBuiltinWidget(widgets, VI_KILL_EOL, this::viKillEol);
        addBuiltinWidget(widgets, VI_FIRST_NON_BLANK, this::viFirstNonBlank);
        addBuiltinWidget(widgets, VI_FIND_NEXT_CHAR, this::viFindNextChar);
        addBuiltinWidget(widgets, VI_FIND_NEXT_CHAR_SKIP, this::viFindNextCharSkip);
        addBuiltinWidget(widgets, VI_FIND_PREV_CHAR, this::viFindPrevChar);
        addBuiltinWidget(widgets, VI_FIND_PREV_CHAR_SKIP, this::viFindPrevCharSkip);
        addBuiltinWidget(widgets, VI_FORWARD_BLANK_WORD, this::viForwardBlankWord);
        addBuiltinWidget(widgets, VI_FORWARD_BLANK_WORD_END, this::viForwardBlankWordEnd);
        addBuiltinWidget(widgets, VI_FORWARD_CHAR, this::viForwardChar);
        addBuiltinWidget(widgets, VI_FORWARD_WORD, this::viForwardWord);
        addBuiltinWidget(widgets, VI_FORWARD_WORD, this::viForwardWord);
        addBuiltinWidget(widgets, VI_FORWARD_WORD_END, this::viForwardWordEnd);
        addBuiltinWidget(widgets, VI_HISTORY_SEARCH_BACKWARD, this::viHistorySearchBackward);
        addBuiltinWidget(widgets, VI_HISTORY_SEARCH_FORWARD, this::viHistorySearchForward);
        addBuiltinWidget(widgets, VI_INSERT, this::viInsert);
        addBuiltinWidget(widgets, VI_INSERT_BOL, this::viInsertBol);
        addBuiltinWidget(widgets, VI_INSERT_COMMENT, this::viInsertComment);
        addBuiltinWidget(widgets, VI_JOIN, this::viJoin);
        addBuiltinWidget(widgets, VI_KILL_LINE, this::viKillWholeLine);
        addBuiltinWidget(widgets, VI_MATCH_BRACKET, this::viMatchBracket);
        addBuiltinWidget(widgets, VI_OPEN_LINE_ABOVE, this::viOpenLineAbove);
        addBuiltinWidget(widgets, VI_OPEN_LINE_BELOW, this::viOpenLineBelow);
        addBuiltinWidget(widgets, VI_PUT_AFTER, this::viPutAfter);
        addBuiltinWidget(widgets, VI_PUT_BEFORE, this::viPutBefore);
        addBuiltinWidget(widgets, VI_REPEAT_FIND, this::viRepeatFind);
        addBuiltinWidget(widgets, VI_REPEAT_SEARCH, this::viRepeatSearch);
        addBuiltinWidget(widgets, VI_REPLACE_CHARS, this::viReplaceChars);
        addBuiltinWidget(widgets, VI_REV_REPEAT_FIND, this::viRevRepeatFind);
        addBuiltinWidget(widgets, VI_REV_REPEAT_SEARCH, this::viRevRepeatSearch);
        addBuiltinWidget(widgets, VI_SWAP_CASE, this::viSwapCase);
        addBuiltinWidget(widgets, VI_UP_LINE_OR_HISTORY, this::viUpLineOrHistory);
        addBuiltinWidget(widgets, VI_YANK, this::viYankTo);
        addBuiltinWidget(widgets, VI_YANK_WHOLE_LINE, this::viYankWholeLine);
        addBuiltinWidget(widgets, VISUAL_LINE_MODE, this::visualLineMode);
        addBuiltinWidget(widgets, VISUAL_MODE, this::visualMode);
        addBuiltinWidget(widgets, WHAT_CURSOR_POSITION, this::whatCursorPosition);
        addBuiltinWidget(widgets, YANK, this::yank);
        addBuiltinWidget(widgets, YANK_POP, this::yankPop);
        addBuiltinWidget(widgets, MOUSE, this::mouse);
        addBuiltinWidget(widgets, BEGIN_PASTE, this::beginPaste);
        addBuiltinWidget(widgets, FOCUS_IN, this::focusIn);
        addBuiltinWidget(widgets, FOCUS_OUT, this::focusOut);
        return widgets;
    }

    private void addBuiltinWidget(Map widgets, String name, Widget widget) {
        widgets.put(name, namedWidget("." + name, widget));
    }

    private Widget namedWidget(String name, Widget widget) {
        return new Widget() {
            @Override
            public String toString() {
                return name;
            }

            @Override
            public boolean apply() {
                return widget.apply();
            }
        };
    }

    public boolean redisplay() {
        redisplay(true);
        return true;
    }

    protected void redisplay(boolean flush) {
        try {
            lock.lock();

            if (skipRedisplay) {
                skipRedisplay = false;
                return;
            }

            Status status = Status.getStatus(terminal, false);
            if (status != null) {
                if (terminal.getType().startsWith(AbstractWindowsTerminal.TYPE_WINDOWS)) {
                    status.resize();
                }
                status.redraw();
            }

            if (size.getRows() > 0 && size.getRows() < MIN_ROWS) {
                AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH);

                sb.append(prompt);
                concat(getHighlightedBuffer(buf.toString()).columnSplitLength(Integer.MAX_VALUE), sb);
                AttributedString full = sb.toAttributedString();

                sb.setLength(0);
                sb.append(prompt);
                String line = buf.upToCursor();
                if (maskingCallback != null) {
                    line = maskingCallback.display(line);
                }

                concat(new AttributedString(line).columnSplitLength(Integer.MAX_VALUE), sb);
                AttributedString toCursor = sb.toAttributedString();

                int w = WCWidth.wcwidth('…');
                int width = size.getColumns();
                int cursor = toCursor.columnLength();
                int inc = width / 2 + 1;
                while (cursor <= smallTerminalOffset + w) {
                    smallTerminalOffset -= inc;
                }
                while (cursor >= smallTerminalOffset + width - w) {
                    smallTerminalOffset += inc;
                }
                if (smallTerminalOffset > 0) {
                    sb.setLength(0);
                    sb.append("…");
                    sb.append(full.columnSubSequence(smallTerminalOffset + w, Integer.MAX_VALUE));
                    full = sb.toAttributedString();
                }
                int length = full.columnLength();
                if (length >= smallTerminalOffset + width) {
                    sb.setLength(0);
                    sb.append(full.columnSubSequence(0, width - w));
                    sb.append("…");
                    full = sb.toAttributedString();
                }

                display.update(Collections.singletonList(full), cursor - smallTerminalOffset, flush);
                return;
            }

            List secondaryPrompts = new ArrayList<>();
            AttributedString full = getDisplayedBufferWithPrompts(secondaryPrompts);

            List newLines;
            if (size.getColumns() <= 0) {
                newLines = new ArrayList<>();
                newLines.add(full);
            } else {
                newLines = full.columnSplitLength(size.getColumns(), true, display.delayLineWrap());
            }

            List rightPromptLines;
            if (rightPrompt.length() == 0 || size.getColumns() <= 0) {
                rightPromptLines = new ArrayList<>();
            } else {
                rightPromptLines = rightPrompt.columnSplitLength(size.getColumns());
            }
            while (newLines.size() < rightPromptLines.size()) {
                newLines.add(new AttributedString(""));
            }
            for (int i = 0; i < rightPromptLines.size(); i++) {
                AttributedString line = rightPromptLines.get(i);
                newLines.set(i, addRightPrompt(line, newLines.get(i)));
            }

            int cursorPos = -1;
            int cursorNewLinesId = -1;
            int cursorColPos = -1;
            if (size.getColumns() > 0) {
                AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH);
                sb.append(prompt);
                String buffer = buf.upToCursor();
                if (maskingCallback != null) {
                    buffer = maskingCallback.display(buffer);
                }
                sb.append(insertSecondaryPrompts(new AttributedString(buffer), secondaryPrompts, false));
                List promptLines =
                        sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap());
                if (!promptLines.isEmpty()) {
                    cursorNewLinesId = promptLines.size() - 1;
                    cursorColPos = promptLines.get(promptLines.size() - 1).columnLength();
                    cursorPos = size.cursorPos(cursorNewLinesId, cursorColPos);
                }
            }

            List newLinesToDisplay = new ArrayList<>();
            int displaySize = displayRows(status);
            if (newLines.size() > displaySize && !isTerminalDumb()) {
                StringBuilder sb = new StringBuilder(">....");
                // blanks are needed when displaying command completion candidate list
                for (int i = sb.toString().length(); i < size.getColumns(); i++) {
                    sb.append(" ");
                }
                AttributedString partialCommandInfo = new AttributedString(sb.toString());
                int lineId = newLines.size() - displaySize + 1;
                int endId = displaySize;
                int startId = 1;
                if (lineId > cursorNewLinesId) {
                    lineId = cursorNewLinesId;
                    endId = displaySize - 1;
                    startId = 0;
                } else {
                    newLinesToDisplay.add(partialCommandInfo);
                }
                int cursorRowPos = 0;
                for (int i = startId; i < endId; i++) {
                    if (cursorNewLinesId == lineId) {
                        cursorRowPos = i;
                    }
                    newLinesToDisplay.add(newLines.get(lineId++));
                }
                if (startId == 0) {
                    newLinesToDisplay.add(partialCommandInfo);
                }
                cursorPos = size.cursorPos(cursorRowPos, cursorColPos);
            } else {
                newLinesToDisplay = newLines;
            }
            display.update(newLinesToDisplay, cursorPos, flush);
        } finally {
            lock.unlock();
        }
    }

    private void concat(List lines, AttributedStringBuilder sb) {
        if (lines.size() > 1) {
            for (int i = 0; i < lines.size() - 1; i++) {
                sb.append(lines.get(i));
                sb.style(sb.style().inverse());
                sb.append("\\n");
                sb.style(sb.style().inverseOff());
            }
        }
        sb.append(lines.get(lines.size() - 1));
    }

    private String matchPreviousCommand(String buffer) {
        if (buffer.length() == 0) {
            return "";
        }
        History history = getHistory();
        StringBuilder sb = new StringBuilder();
        for (char c : buffer.replace("\\", "\\\\").toCharArray()) {
            if (c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '^' || c == '*' || c == '$'
                    || c == '.' || c == '?' || c == '+' || c == '|' || c == '<' || c == '>' || c == '!' || c == '-') {
                sb.append('\\');
            }
            sb.append(c);
        }
        Pattern pattern = Pattern.compile(sb.toString() + ".*", Pattern.DOTALL);
        Iterator iter = history.reverseIterator(history.last());
        String suggestion = "";
        int tot = 0;
        while (iter.hasNext()) {
            History.Entry entry = iter.next();
            Matcher matcher = pattern.matcher(entry.line());
            if (matcher.matches()) {
                suggestion = entry.line().substring(buffer.length());
                break;
            } else if (tot > 200) {
                break;
            }
            tot++;
        }
        return suggestion;
    }

    /**
     * Compute the full string to be displayed with the left, right and secondary prompts
     * @param secondaryPrompts a list to store the secondary prompts
     * @return the displayed string including the buffer, left prompts and the help below
     */
    public AttributedString getDisplayedBufferWithPrompts(List secondaryPrompts) {
        AttributedString attBuf = getHighlightedBuffer(buf.toString());

        AttributedString tNewBuf = insertSecondaryPrompts(attBuf, secondaryPrompts);
        AttributedStringBuilder full = new AttributedStringBuilder().tabs(TAB_WIDTH);
        full.append(prompt);
        full.append(tNewBuf);
        if (doAutosuggestion && !isTerminalDumb()) {
            String lastBinding = getLastBinding() != null ? getLastBinding() : "";
            if (autosuggestion == SuggestionType.HISTORY) {
                AttributedStringBuilder sb = new AttributedStringBuilder();
                tailTip = matchPreviousCommand(buf.toString());
                sb.styled(AttributedStyle::faint, tailTip);
                full.append(sb.toAttributedString());
            } else if (autosuggestion == SuggestionType.COMPLETER) {
                if (buf.length() >= getInt(SUGGESTIONS_MIN_BUFFER_SIZE, DEFAULT_SUGGESTIONS_MIN_BUFFER_SIZE)
                        && buf.length() == buf.cursor()
                        && (!lastBinding.equals("\t") || buf.prevChar() == ' ' || buf.prevChar() == '=')) {
                    clearChoices();
                    listChoices(true);
                } else if (!lastBinding.equals("\t")) {
                    clearChoices();
                }
            } else if (autosuggestion == SuggestionType.TAIL_TIP) {
                if (buf.length() == buf.cursor()) {
                    if (!lastBinding.equals("\t") || buf.prevChar() == ' ') {
                        clearChoices();
                    }
                    AttributedStringBuilder sb = new AttributedStringBuilder();
                    if (buf.prevChar() != ' ') {
                        if (!tailTip.startsWith("[")) {
                            int idx = tailTip.indexOf(' ');
                            int idb = buf.toString().lastIndexOf(' ');
                            int idd = buf.toString().lastIndexOf('-');
                            if (idx > 0 && ((idb == -1 && idb == idd) || (idb >= 0 && idb > idd))) {
                                tailTip = tailTip.substring(idx);
                            } else if (idb >= 0 && idb < idd) {
                                sb.append(" ");
                            }
                        } else {
                            sb.append(" ");
                        }
                    }
                    sb.styled(AttributedStyle::faint, tailTip);
                    full.append(sb.toAttributedString());
                }
            }
        }
        if (post != null) {
            full.append("\n");
            full.append(post.get());
        }
        doAutosuggestion = true;
        return full.toAttributedString();
    }

    private AttributedString getHighlightedBuffer(String buffer) {
        if (maskingCallback != null) {
            buffer = maskingCallback.display(buffer);
        }
        if (highlighter != null
                && !isSet(Option.DISABLE_HIGHLIGHTER)
                && buffer.length() < getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE)) {
            return highlighter.highlight(this, buffer);
        }
        return new AttributedString(buffer);
    }

    private AttributedString expandPromptPattern(String pattern, int padToWidth, String message, int line) {
        ArrayList parts = new ArrayList<>();
        boolean isHidden = false;
        int padPartIndex = -1;
        StringBuilder padPartString = null;
        StringBuilder sb = new StringBuilder();
        // Add "%{" to avoid special case for end of string.
        pattern = pattern + "%{";
        int plen = pattern.length();
        int padChar = -1;
        int padPos = -1;
        int cols = 0;
        for (int i = 0; i < plen; ) {
            char ch = pattern.charAt(i++);
            if (ch == '%' && i < plen) {
                int count = 0;
                boolean countSeen = false;
                decode:
                while (true) {
                    ch = pattern.charAt(i++);
                    switch (ch) {
                        case '{':
                        case '}':
                            String str = sb.toString();
                            AttributedString astr;
                            if (!isHidden) {
                                astr = fromAnsi(str);
                                cols += astr.columnLength();
                            } else {
                                astr = new AttributedString(str, AttributedStyle.HIDDEN);
                            }
                            if (padPartIndex == parts.size()) {
                                padPartString = sb;
                                if (i < plen) {
                                    sb = new StringBuilder();
                                }
                            } else {
                                sb.setLength(0);
                            }
                            parts.add(astr);
                            isHidden = ch == '{';
                            break decode;
                        case '%':
                            sb.append(ch);
                            break decode;
                        case 'N':
                            sb.append(getInt(LINE_OFFSET, 0) + line);
                            break decode;
                        case 'M':
                            if (message != null) sb.append(message);
                            break decode;
                        case 'P':
                            if (countSeen && count >= 0) padToWidth = count;
                            if (i < plen) {
                                padChar = pattern.charAt(i++);
                                // FIXME check surrogate
                            }
                            padPos = sb.length();
                            padPartIndex = parts.size();
                            break decode;
                        case '-':
                        case '0':
                        case '1':
                        case '2':
                        case '3':
                        case '4':
                        case '5':
                        case '6':
                        case '7':
                        case '8':
                        case '9':
                            boolean neg = false;
                            if (ch == '-') {
                                neg = true;
                                ch = pattern.charAt(i++);
                            }
                            countSeen = true;
                            count = 0;
                            while (ch >= '0' && ch <= '9') {
                                count = (count < 0 ? 0 : 10 * count) + (ch - '0');
                                ch = pattern.charAt(i++);
                            }
                            if (neg) {
                                count = -count;
                            }
                            i--;
                            break;
                        default:
                            break decode;
                    }
                }
            } else sb.append(ch);
        }
        if (padToWidth > cols) {
            int padCharCols = WCWidth.wcwidth(padChar);
            int padCount = (padToWidth - cols) / padCharCols;
            sb = padPartString;
            while (--padCount >= 0) sb.insert(padPos, (char) padChar); // FIXME if wide
            parts.set(padPartIndex, fromAnsi(sb.toString()));
        }
        return AttributedString.join(null, parts);
    }

    private AttributedString fromAnsi(String str) {
        return AttributedString.fromAnsi(str, Collections.singletonList(0), alternateIn, alternateOut);
    }

    private AttributedString insertSecondaryPrompts(AttributedString str, List prompts) {
        return insertSecondaryPrompts(str, prompts, true);
    }

    private AttributedString insertSecondaryPrompts(
            AttributedString strAtt, List prompts, boolean computePrompts) {
        Objects.requireNonNull(prompts);
        List lines = strAtt.columnSplitLength(Integer.MAX_VALUE);
        AttributedStringBuilder sb = new AttributedStringBuilder();
        String secondaryPromptPattern = getString(SECONDARY_PROMPT_PATTERN, DEFAULT_SECONDARY_PROMPT_PATTERN);
        boolean needsMessage = secondaryPromptPattern.contains("%M")
                && strAtt.length() < getInt(FEATURES_MAX_BUFFER_SIZE, DEFAULT_FEATURES_MAX_BUFFER_SIZE);
        AttributedStringBuilder buf = new AttributedStringBuilder();
        int width = 0;
        List missings = new ArrayList<>();
        if (computePrompts && secondaryPromptPattern.contains("%P")) {
            width = prompt.columnLength();
            if (width > size.getColumns() || prompt.contains('\n')) {
                width = new TerminalLine(prompt.toString(), 0, size.getColumns())
                        .getEndLine()
                        .length();
            }
            for (int line = 0; line < lines.size() - 1; line++) {
                AttributedString prompt;
                buf.append(lines.get(line)).append("\n");
                String missing = "";
                if (needsMessage) {
                    try {
                        parser.parse(buf.toString(), buf.length(), ParseContext.SECONDARY_PROMPT);
                    } catch (EOFError e) {
                        missing = e.getMissing();
                    } catch (SyntaxError e) {
                        // Ignore
                    }
                }
                missings.add(missing);
                prompt = expandPromptPattern(secondaryPromptPattern, 0, missing, line + 1);
                width = Math.max(width, prompt.columnLength());
            }
            buf.setLength(0);
        }
        int line = 0;
        while (line < lines.size() - 1) {
            sb.append(lines.get(line)).append("\n");
            buf.append(lines.get(line)).append("\n");
            AttributedString prompt;
            if (computePrompts) {
                String missing = "";
                if (needsMessage) {
                    if (missings.isEmpty()) {
                        try {
                            parser.parse(buf.toString(), buf.length(), ParseContext.SECONDARY_PROMPT);
                        } catch (EOFError e) {
                            missing = e.getMissing();
                        } catch (SyntaxError e) {
                            // Ignore
                        }
                    } else {
                        missing = missings.get(line);
                    }
                }
                prompt = expandPromptPattern(secondaryPromptPattern, width, missing, line + 1);
            } else {
                prompt = prompts.get(line);
            }
            prompts.add(prompt);
            sb.append(prompt);
            line++;
        }
        sb.append(lines.get(line));
        buf.append(lines.get(line));
        return sb.toAttributedString();
    }

    private AttributedString addRightPrompt(AttributedString prompt, AttributedString line) {
        int width = prompt.columnLength();
        boolean endsWithNl = line.length() > 0 && line.charAt(line.length() - 1) == '\n';
        // columnLength counts -1 for the final newline; adjust for that
        int nb = size.getColumns() - width - (line.columnLength() + (endsWithNl ? 1 : 0));
        if (nb >= 3) {
            AttributedStringBuilder sb = new AttributedStringBuilder(size.getColumns());
            sb.append(line, 0, endsWithNl ? line.length() - 1 : line.length());
            for (int j = 0; j < nb; j++) {
                sb.append(' ');
            }
            sb.append(prompt);
            if (endsWithNl) {
                sb.append('\n');
            }
            line = sb.toAttributedString();
        }
        return line;
    }

    //
    // Completion
    //

    protected boolean insertTab() {
        return isSet(Option.INSERT_TAB)
                && getLastBinding().equals("\t")
                && buf.toString().matches("(^|[\\s\\S]*\n)[\r\n\t ]*");
    }

    protected boolean expandHistory() {
        String str = buf.toString();
        String exp = expander.expandHistory(history, str);
        if (!exp.equals(str)) {
            buf.clear();
            buf.write(exp);
            return true;
        } else {
            return false;
        }
    }

    protected enum CompletionType {
        Expand,
        ExpandComplete,
        Complete,
        List,
    }

    protected boolean expandWord() {
        if (insertTab()) {
            return selfInsert();
        } else {
            return doComplete(CompletionType.Expand, isSet(Option.MENU_COMPLETE), false);
        }
    }

    protected boolean expandOrComplete() {
        if (insertTab()) {
            return selfInsert();
        } else {
            return doComplete(CompletionType.ExpandComplete, isSet(Option.MENU_COMPLETE), false);
        }
    }

    protected boolean expandOrCompletePrefix() {
        if (insertTab()) {
            return selfInsert();
        } else {
            return doComplete(CompletionType.ExpandComplete, isSet(Option.MENU_COMPLETE), true);
        }
    }

    protected boolean completeWord() {
        if (insertTab()) {
            return selfInsert();
        } else {
            return doComplete(CompletionType.Complete, isSet(Option.MENU_COMPLETE), false);
        }
    }

    protected boolean menuComplete() {
        if (insertTab()) {
            return selfInsert();
        } else {
            return doComplete(CompletionType.Complete, true, false);
        }
    }

    protected boolean menuExpandOrComplete() {
        if (insertTab()) {
            return selfInsert();
        } else {
            return doComplete(CompletionType.ExpandComplete, true, false);
        }
    }

    protected boolean completePrefix() {
        if (insertTab()) {
            return selfInsert();
        } else {
            return doComplete(CompletionType.Complete, isSet(Option.MENU_COMPLETE), true);
        }
    }

    protected boolean listChoices() {
        return listChoices(false);
    }

    private boolean listChoices(boolean forSuggestion) {
        return doComplete(CompletionType.List, isSet(Option.MENU_COMPLETE), false, forSuggestion);
    }

    protected boolean deleteCharOrList() {
        if (buf.cursor() != buf.length() || buf.length() == 0) {
            return deleteChar();
        } else {
            return doComplete(CompletionType.List, isSet(Option.MENU_COMPLETE), false);
        }
    }

    protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix) {
        return doComplete(lst, useMenu, prefix, false);
    }

    protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix, boolean forSuggestion) {
        // If completion is disabled, just bail out
        if (getBoolean(DISABLE_COMPLETION, false)) {
            return true;
        }
        // Try to expand history first
        // If there is actually an expansion, bail out now
        if (!isSet(Option.DISABLE_EVENT_EXPANSION)) {
            try {
                if (expandHistory()) {
                    return true;
                }
            } catch (Exception e) {
                Log.info("Error while expanding history", e);
                return false;
            }
        }

        // Parse the command line
        CompletingParsedLine line;
        try {
            line = wrap(parser.parse(buf.toString(), buf.cursor(), ParseContext.COMPLETE));
        } catch (Exception e) {
            Log.info("Error while parsing line", e);
            return false;
        }

        // Find completion candidates
        List candidates = new ArrayList<>();
        try {
            if (completer != null) {
                completer.complete(this, line, candidates);
            }
        } catch (Exception e) {
            Log.info("Error while finding completion candidates", e);
            if (Log.isDebugEnabled()) {
                e.printStackTrace();
            }
            return false;
        }

        if (lst == CompletionType.ExpandComplete || lst == CompletionType.Expand) {
            String w = expander.expandVar(line.word());
            if (!line.word().equals(w)) {
                if (prefix) {
                    buf.backspace(line.wordCursor());
                } else {
                    buf.move(line.word().length() - line.wordCursor());
                    buf.backspace(line.word().length());
                }
                buf.write(w);
                return true;
            }
            if (lst == CompletionType.Expand) {
                return false;
            } else {
                lst = CompletionType.Complete;
            }
        }

        boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE);
        int errors = getInt(ERRORS, DEFAULT_ERRORS);

        completionMatcher.compile(options, prefix, line, caseInsensitive, errors, getOriginalGroupName());
        // Find matching candidates
        List possible = completionMatcher.matches(candidates);
        // If we have no matches, bail out
        if (possible.isEmpty()) {
            return false;
        }
        size.copy(terminal.getSize());
        try {
            // If we only need to display the list, do it now
            if (lst == CompletionType.List) {
                doList(possible, line.word(), false, line::escape, forSuggestion);
                return !possible.isEmpty();
            }

            // Check if there's a single possible match
            Candidate completion = null;
            // If there's a single possible completion
            if (possible.size() == 1) {
                completion = possible.get(0);
            }
            // Or if RECOGNIZE_EXACT is set, try to find an exact match
            else if (isSet(Option.RECOGNIZE_EXACT)) {
                completion = completionMatcher.exactMatch();
            }
            // Complete and exit
            if (completion != null && !completion.value().isEmpty()) {
                if (prefix) {
                    buf.backspace(line.rawWordCursor());
                } else {
                    buf.move(line.rawWordLength() - line.rawWordCursor());
                    buf.backspace(line.rawWordLength());
                }
                buf.write(line.escape(completion.value(), completion.complete()));
                if (completion.complete()) {
                    if (buf.currChar() != ' ') {
                        buf.write(" ");
                    } else {
                        buf.move(1);
                    }
                }
                if (completion.suffix() != null) {
                    if (autosuggestion == SuggestionType.COMPLETER) {
                        listChoices(true);
                    }
                    redisplay();
                    Binding op = readBinding(getKeys());
                    if (op != null) {
                        String chars = getString(REMOVE_SUFFIX_CHARS, DEFAULT_REMOVE_SUFFIX_CHARS);
                        String ref = op instanceof Reference ? ((Reference) op).name() : null;
                        if (SELF_INSERT.equals(ref)
                                        && chars.indexOf(getLastBinding().charAt(0)) >= 0
                                || ACCEPT_LINE.equals(ref)) {
                            buf.backspace(completion.suffix().length());
                            if (getLastBinding().charAt(0) != ' ') {
                                buf.write(' ');
                            }
                        }
                        pushBackBinding(true);
                    }
                }
                return true;
            }

            if (useMenu) {
                buf.move(line.word().length() - line.wordCursor());
                buf.backspace(line.word().length());
                doMenu(possible, line.word(), line::escape);
                return true;
            }

            // Find current word and move to end
            String current;
            if (prefix) {
                current = line.word().substring(0, line.wordCursor());
            } else {
                current = line.word();
                buf.move(line.rawWordLength() - line.rawWordCursor());
            }
            // Now, we need to find the unambiguous completion
            // TODO: need to find common suffix
            String commonPrefix = completionMatcher.getCommonPrefix();
            boolean hasUnambiguous = commonPrefix.startsWith(current) && !commonPrefix.equals(current);

            if (hasUnambiguous) {
                buf.backspace(line.rawWordLength());
                buf.write(line.escape(commonPrefix, false));
                callWidget(REDISPLAY);
                current = commonPrefix;
                if ((!isSet(Option.AUTO_LIST) && isSet(Option.AUTO_MENU))
                        || (isSet(Option.AUTO_LIST) && isSet(Option.LIST_AMBIGUOUS))) {
                    if (!nextBindingIsComplete()) {
                        return true;
                    }
                }
            }
            if (isSet(Option.AUTO_LIST)) {
                if (!doList(possible, current, true, line::escape)) {
                    return true;
                }
            }
            if (isSet(Option.AUTO_MENU)) {
                buf.backspace(current.length());
                doMenu(possible, line.word(), line::escape);
            }
            return true;
        } finally {
            size.copy(terminal.getBufferSize());
        }
    }

    protected static CompletingParsedLine wrap(ParsedLine line) {
        if (line instanceof CompletingParsedLine) {
            return (CompletingParsedLine) line;
        } else {
            return new CompletingParsedLine() {
                public String word() {
                    return line.word();
                }

                public int wordCursor() {
                    return line.wordCursor();
                }

                public int wordIndex() {
                    return line.wordIndex();
                }

                public List words() {
                    return line.words();
                }

                public String line() {
                    return line.line();
                }

                public int cursor() {
                    return line.cursor();
                }

                public CharSequence escape(CharSequence candidate, boolean complete) {
                    return candidate;
                }

                public int rawWordCursor() {
                    return wordCursor();
                }

                public int rawWordLength() {
                    return word().length();
                }
            };
        }
    }

    protected Comparator getCandidateComparator(boolean caseInsensitive, String word) {
        String wdi = caseInsensitive ? word.toLowerCase() : word;
        ToIntFunction wordDistance = w -> ReaderUtils.distance(wdi, caseInsensitive ? w.toLowerCase() : w);
        return Comparator.comparing(Candidate::value, Comparator.comparingInt(wordDistance))
                .thenComparing(Comparator.naturalOrder());
    }

    protected String getOthersGroupName() {
        return getString(OTHERS_GROUP_NAME, DEFAULT_OTHERS_GROUP_NAME);
    }

    protected String getOriginalGroupName() {
        return getString(ORIGINAL_GROUP_NAME, DEFAULT_ORIGINAL_GROUP_NAME);
    }

    protected Comparator getGroupComparator() {
        return Comparator.comparingInt(s -> getOthersGroupName().equals(s)
                        ? 1
                        : getOriginalGroupName().equals(s) ? -1 : 0)
                .thenComparing(String::toLowerCase, Comparator.naturalOrder());
    }

    private void mergeCandidates(List possible) {
        // Merge candidates if the have the same key
        Map> keyedCandidates = new HashMap<>();
        for (Candidate candidate : possible) {
            if (candidate.key() != null) {
                List cands = keyedCandidates.computeIfAbsent(candidate.key(), s -> new ArrayList<>());
                cands.add(candidate);
            }
        }
        if (!keyedCandidates.isEmpty()) {
            for (List candidates : keyedCandidates.values()) {
                if (candidates.size() >= 1) {
                    possible.removeAll(candidates);
                    // Candidates with the same key are supposed to have
                    // the same description
                    candidates.sort(Comparator.comparing(Candidate::value));
                    Candidate first = candidates.get(0);
                    String disp = candidates.stream().map(Candidate::displ).collect(Collectors.joining(" "));
                    possible.add(new Candidate(
                            first.value(), disp, first.group(), first.descr(), first.suffix(), null, first.complete()));
                }
            }
        }
    }

    protected boolean nextBindingIsComplete() {
        redisplay();
        KeyMap keyMap = keyMaps.get(MENU);
        Binding operation = readBinding(getKeys(), keyMap);
        if (operation instanceof Reference && MENU_COMPLETE.equals(((Reference) operation).name())) {
            return true;
        } else {
            pushBackBinding();
            return false;
        }
    }

    private int displayRows() {
        return displayRows(Status.getStatus(terminal, false));
    }

    private int displayRows(Status status) {
        return size.getRows() - (status != null ? status.size() : 0);
    }

    private int visibleDisplayRows() {
        Status status = Status.getStatus(terminal, false);
        return terminal.getSize().getRows() - (status != null ? status.size() : 0);
    }

    private int promptLines() {
        AttributedString text =
                insertSecondaryPrompts(AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>());
        return text.columnSplitLength(size.getColumns(), false, display.delayLineWrap())
                .size();
    }

    private class MenuSupport implements Supplier {
        final List possible;
        final BiFunction escaper;
        int selection;
        int topLine;
        String word;
        AttributedString computed;
        int lines;
        int columns;
        String completed;

        public MenuSupport(
                List original, String completed, BiFunction escaper) {
            this.possible = new ArrayList<>();
            this.escaper = escaper;
            this.selection = -1;
            this.topLine = 0;
            this.word = "";
            this.completed = completed;
            computePost(original, null, possible, completed);
            next();
        }

        public Candidate completion() {
            return possible.get(selection);
        }

        public void next() {
            selection = (selection + 1) % possible.size();
            update();
        }

        public void previous() {
            selection = (selection + possible.size() - 1) % possible.size();
            update();
        }

        /**
         * Move 'step' options along the major axis of the menu.

* ie. if the menu is listing rows first, change row (up/down); * otherwise move column (left/right) * * @param step number of options to move by */ private void major(int step) { int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : lines; int sel = selection + step * axis; if (sel < 0) { int pos = (sel + axis) % axis; // needs +axis as (-1)%x == -1 int remainders = possible.size() % axis; sel = possible.size() - remainders + pos; if (sel >= possible.size()) { sel -= axis; } } else if (sel >= possible.size()) { sel = sel % axis; } selection = sel; update(); } /** * Move 'step' options along the minor axis of the menu.

* ie. if the menu is listing rows first, move along the row (left/right); * otherwise move along the column (up/down) * * @param step number of options to move by */ private void minor(int step) { int axis = isSet(Option.LIST_ROWS_FIRST) ? columns : lines; int row = selection % axis; int options = possible.size(); if (selection - row + axis > options) { // selection is the last row/column // so there are fewer options than other rows axis = options % axis; } selection = selection - row + ((axis + row + step) % axis); update(); } public void up() { if (isSet(Option.LIST_ROWS_FIRST)) { major(-1); } else { minor(-1); } } public void down() { if (isSet(Option.LIST_ROWS_FIRST)) { major(1); } else { minor(1); } } public void left() { if (isSet(Option.LIST_ROWS_FIRST)) { minor(-1); } else { major(-1); } } public void right() { if (isSet(Option.LIST_ROWS_FIRST)) { minor(1); } else { major(1); } } private void update() { buf.backspace(word.length()); word = escaper.apply(completion().value(), true).toString(); buf.write(word); // Compute displayed prompt PostResult pr = computePost(possible, completion(), null, completed); int displaySize = displayRows() - promptLines(); if (pr.lines > displaySize) { int displayed = displaySize - 1; if (pr.selectedLine >= 0) { if (pr.selectedLine < topLine) { topLine = pr.selectedLine; } else if (pr.selectedLine >= topLine + displayed) { topLine = pr.selectedLine - displayed + 1; } } AttributedString post = pr.post; if (post.length() > 0 && post.charAt(post.length() - 1) != '\n') { post = new AttributedStringBuilder(post.length() + 1) .append(post) .append("\n") .toAttributedString(); } List lines = post.columnSplitLength(size.getColumns(), true, display.delayLineWrap()); List sub = new ArrayList<>(lines.subList(topLine, topLine + displayed)); sub.add(new AttributedStringBuilder() .style(AttributedStyle.DEFAULT.foreground(AttributedStyle.CYAN)) .append("rows ") .append(Integer.toString(topLine + 1)) .append(" to ") .append(Integer.toString(topLine + displayed)) .append(" of ") .append(Integer.toString(lines.size())) .append("\n") .style(AttributedStyle.DEFAULT) .toAttributedString()); computed = AttributedString.join(AttributedString.EMPTY, sub); } else { computed = pr.post; } lines = pr.lines; columns = (possible.size() + lines - 1) / lines; } @Override public AttributedString get() { return computed; } } protected boolean doMenu( List original, String completed, BiFunction escaper) { // Reorder candidates according to display order final List possible = new ArrayList<>(); boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE); original.sort(getCandidateComparator(caseInsensitive, completed)); mergeCandidates(original); computePost(original, null, possible, completed); // candidate grouping is not supported by MenuSupport boolean defaultAutoGroup = isSet(Option.AUTO_GROUP); boolean defaultGroup = isSet(Option.GROUP); if (!isSet(Option.GROUP_PERSIST)) { option(Option.AUTO_GROUP, false); option(Option.GROUP, false); } // Build menu support MenuSupport menuSupport = new MenuSupport(original, completed, escaper); post = menuSupport; callWidget(REDISPLAY); // Loop KeyMap keyMap = keyMaps.get(MENU); Binding operation; while ((operation = readBinding(getKeys(), keyMap)) != null) { String ref = (operation instanceof Reference) ? ((Reference) operation).name() : ""; switch (ref) { case MENU_COMPLETE: menuSupport.next(); break; case REVERSE_MENU_COMPLETE: menuSupport.previous(); break; case UP_LINE_OR_HISTORY: case UP_LINE_OR_SEARCH: menuSupport.up(); break; case DOWN_LINE_OR_HISTORY: case DOWN_LINE_OR_SEARCH: menuSupport.down(); break; case FORWARD_CHAR: menuSupport.right(); break; case BACKWARD_CHAR: menuSupport.left(); break; case CLEAR_SCREEN: clearScreen(); break; default: { Candidate completion = menuSupport.completion(); if (completion.suffix() != null) { String chars = getString(REMOVE_SUFFIX_CHARS, DEFAULT_REMOVE_SUFFIX_CHARS); if (SELF_INSERT.equals(ref) && chars.indexOf(getLastBinding().charAt(0)) >= 0 || BACKWARD_DELETE_CHAR.equals(ref)) { buf.backspace(completion.suffix().length()); } } if (completion.complete() && getLastBinding().charAt(0) != ' ' && (SELF_INSERT.equals(ref) || getLastBinding().charAt(0) != ' ')) { buf.write(' '); } if (!ACCEPT_LINE.equals(ref) && !(SELF_INSERT.equals(ref) && completion.suffix() != null && completion.suffix().startsWith(getLastBinding()))) { pushBackBinding(true); } post = null; option(Option.AUTO_GROUP, defaultAutoGroup); option(Option.GROUP, defaultGroup); return true; } } doAutosuggestion = false; callWidget(REDISPLAY); } option(Option.AUTO_GROUP, defaultAutoGroup); option(Option.GROUP, defaultGroup); return false; } protected boolean clearChoices() { return doList(new ArrayList<>(), "", false, null, false); } protected boolean doList( List possible, String completed, boolean runLoop, BiFunction escaper) { return doList(possible, completed, runLoop, escaper, false); } protected boolean doList( List possible, String completed, boolean runLoop, BiFunction escaper, boolean forSuggestion) { // If we list only and if there's a big // number of items, we should ask the user // for confirmation, display the list // and redraw the line at the bottom mergeCandidates(possible); AttributedString text = insertSecondaryPrompts(AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>()); int promptLines = text.columnSplitLength(size.getColumns(), false, display.delayLineWrap()) .size(); PostResult postResult = computePost(possible, null, null, completed); int lines = postResult.lines; int listMax = getInt(LIST_MAX, DEFAULT_LIST_MAX); if (listMax > 0 && possible.size() >= listMax || lines >= size.getRows() - promptLines) { if (!forSuggestion) { // prompt post = () -> new AttributedString(getAppName() + ": do you wish to see all " + possible.size() + " possibilities (" + lines + " lines)?"); redisplay(true); int c = readCharacter(); if (c != 'y' && c != 'Y' && c != '\t') { post = null; return false; } } else { return false; } } boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE); StringBuilder sb = new StringBuilder(); candidateStartPosition = 0; while (true) { String current = completed + sb.toString(); List cands; if (sb.length() > 0) { completionMatcher.compile(options, false, new CompletingWord(current), caseInsensitive, 0, null); cands = completionMatcher.matches(possible).stream() .sorted(getCandidateComparator(caseInsensitive, current)) .collect(Collectors.toList()); } else { cands = possible.stream() .sorted(getCandidateComparator(caseInsensitive, current)) .collect(Collectors.toList()); } if (isSet(Option.AUTO_MENU_LIST) && candidateStartPosition == 0) { candidateStartPosition = candidateStartPosition(cands); } post = () -> { AttributedString t = insertSecondaryPrompts( AttributedStringBuilder.append(prompt, buf.toString()), new ArrayList<>()); int pl = t.columnSplitLength(size.getColumns(), false, display.delayLineWrap()) .size(); PostResult pr = computePost(cands, null, null, current); if (pr.lines >= size.getRows() - pl) { post = null; int oldCursor = buf.cursor(); buf.cursor(buf.length()); redisplay(false); buf.cursor(oldCursor); println(); List ls = pr.post.columnSplitLength(size.getColumns(), false, display.delayLineWrap()); Display d = new Display(terminal, false); d.resize(size.getRows(), size.getColumns()); d.update(ls, -1); println(); redrawLine(); return new AttributedString(""); } return pr.post; }; if (!runLoop) { return false; } redisplay(); // TODO: use a different keyMap ? Binding b = doReadBinding(getKeys(), null); if (b instanceof Reference) { String name = ((Reference) b).name(); if (BACKWARD_DELETE_CHAR.equals(name) || VI_BACKWARD_DELETE_CHAR.equals(name)) { if (sb.length() == 0) { pushBackBinding(); post = null; return false; } else { sb.setLength(sb.length() - 1); buf.backspace(); } } else if (SELF_INSERT.equals(name)) { sb.append(getLastBinding()); callWidget(name); if (cands.isEmpty()) { post = null; return false; } } else if ("\t".equals(getLastBinding())) { if (cands.size() == 1 || sb.length() > 0) { post = null; pushBackBinding(); } else if (isSet(Option.AUTO_MENU)) { buf.backspace(escaper.apply(current, false).length()); doMenu(cands, current, escaper); } return false; } else { pushBackBinding(); post = null; return false; } } else if (b == null) { post = null; return false; } } } private static class CompletingWord implements CompletingParsedLine { private final String word; public CompletingWord(String word) { this.word = word; } @Override public CharSequence escape(CharSequence candidate, boolean complete) { return null; } @Override public int rawWordCursor() { return word.length(); } @Override public int rawWordLength() { return word.length(); } @Override public String word() { return word; } @Override public int wordCursor() { return word.length(); } @Override public int wordIndex() { return 0; } @Override public List words() { return null; } @Override public String line() { return word; } @Override public int cursor() { return word.length(); } } protected static class PostResult { final AttributedString post; final int lines; final int selectedLine; public PostResult(AttributedString post, int lines, int selectedLine) { this.post = post; this.lines = lines; this.selectedLine = selectedLine; } } protected PostResult computePost( List possible, Candidate selection, List ordered, String completed) { return computePost( possible, selection, ordered, completed, display::wcwidth, size.getColumns(), isSet(Option.AUTO_GROUP), isSet(Option.GROUP), isSet(Option.LIST_ROWS_FIRST)); } protected PostResult computePost( List possible, Candidate selection, List ordered, String completed, Function wcwidth, int width, boolean autoGroup, boolean groupName, boolean rowsFirst) { List strings = new ArrayList<>(); boolean customOrder = possible.stream().anyMatch(c -> c.sort() != 0); if (groupName) { Comparator groupComparator = getGroupComparator(); Map> sorted; sorted = groupComparator != null ? new TreeMap<>(groupComparator) : new LinkedHashMap<>(); for (Candidate cand : possible) { String group = cand.group(); sorted.computeIfAbsent(group != null ? group : "", s -> new LinkedHashMap<>()) .put((customOrder ? cand.sort() : cand.value()), cand); } for (Map.Entry> entry : sorted.entrySet()) { String group = entry.getKey(); if (group.isEmpty() && sorted.size() > 1) { group = getOthersGroupName(); } if (!group.isEmpty() && autoGroup) { strings.add(group); } strings.add(new ArrayList<>(entry.getValue().values())); if (ordered != null) { ordered.addAll(entry.getValue().values()); } } } else { Set groups = new LinkedHashSet<>(); TreeMap sorted = new TreeMap<>(); for (Candidate cand : possible) { String group = cand.group(); if (group != null) { groups.add(group); } sorted.put((customOrder ? cand.sort() : cand.value()), cand); } if (autoGroup) { strings.addAll(groups); } strings.add(new ArrayList<>(sorted.values())); if (ordered != null) { ordered.addAll(sorted.values()); } } return toColumns(strings, selection, completed, wcwidth, width, rowsFirst); } private static final String DESC_PREFIX = "("; private static final String DESC_SUFFIX = ")"; private static final int MARGIN_BETWEEN_DISPLAY_AND_DESC = 1; private static final int MARGIN_BETWEEN_COLUMNS = 3; private static final int MENU_LIST_WIDTH = 25; private static class TerminalLine { private String endLine; private int startPos; public TerminalLine(String line, int startPos, int width) { this.startPos = startPos; endLine = line.substring(line.lastIndexOf('\n') + 1); boolean first = true; while (endLine.length() + (first ? startPos : 0) > width && width > 0) { if (first) { endLine = endLine.substring(width - startPos); } else { endLine = endLine.substring(width); } first = false; } if (!first) { this.startPos = 0; } } public int getStartPos() { return startPos; } public String getEndLine() { return endLine; } } private int candidateStartPosition(List cands) { List values = cands.stream() .map(c -> AttributedString.stripAnsi(c.displ())) .filter(c -> !c.matches("\\w+") && c.length() > 1) .collect(Collectors.toList()); Set notDelimiters = new HashSet<>(); values.forEach(v -> v.substring(0, v.length() - 1) .chars() .filter(c -> !Character.isDigit(c) && !Character.isAlphabetic(c)) .forEach(c -> notDelimiters.add(Character.toString((char) c)))); int width = size.getColumns(); int promptLength = prompt != null ? prompt.length() : 0; if (promptLength > 0) { TerminalLine tp = new TerminalLine(prompt.toString(), 0, width); promptLength = tp.getEndLine().length(); } TerminalLine tl = new TerminalLine(buf.substring(0, buf.cursor()), promptLength, width); int out = tl.getStartPos(); String buffer = tl.getEndLine(); for (int i = buffer.length(); i > 0; i--) { if (buffer.substring(0, i).matches(".*\\W") && !notDelimiters.contains(buffer.substring(i - 1, i))) { out += i; break; } } return out; } @SuppressWarnings("unchecked") protected PostResult toColumns( List items, Candidate selection, String completed, Function wcwidth, int width, boolean rowsFirst) { int[] out = new int[2]; // TODO: support Option.LIST_PACKED // Compute column width int maxWidth = 0; int listSize = 0; for (Object item : items) { if (item instanceof String) { int len = wcwidth.apply((String) item); maxWidth = Math.max(maxWidth, len); } else if (item instanceof List) { for (Candidate cand : (List) item) { listSize++; int len = wcwidth.apply(cand.displ()); if (cand.descr() != null) { len += MARGIN_BETWEEN_DISPLAY_AND_DESC; len += DESC_PREFIX.length(); len += wcwidth.apply(cand.descr()); len += DESC_SUFFIX.length(); } maxWidth = Math.max(maxWidth, len); } } } // Build columns AttributedStringBuilder sb = new AttributedStringBuilder(); if (listSize > 0) { if (isSet(Option.AUTO_MENU_LIST) && listSize < Math.min( getInt(MENU_LIST_MAX, DEFAULT_MENU_LIST_MAX), visibleDisplayRows() - promptLines())) { maxWidth = Math.max(maxWidth, MENU_LIST_WIDTH); sb.tabs(Math.max(Math.min(candidateStartPosition, width - maxWidth - 1), 1)); width = maxWidth + 2; if (!isSet(Option.GROUP_PERSIST)) { List list = new ArrayList<>(); for (Object o : items) { if (o instanceof Collection) { list.addAll((Collection) o); } } list = list.stream() .sorted(getCandidateComparator(isSet(Option.CASE_INSENSITIVE), "")) .collect(Collectors.toList()); toColumns(list, width, maxWidth, sb, selection, completed, rowsFirst, true, out); } else { for (Object list : items) { toColumns(list, width, maxWidth, sb, selection, completed, rowsFirst, true, out); } } } else { for (Object list : items) { toColumns(list, width, maxWidth, sb, selection, completed, rowsFirst, false, out); } } } if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\n') { sb.setLength(sb.length() - 1); } return new PostResult(sb.toAttributedString(), out[0], out[1]); } @SuppressWarnings("unchecked") protected void toColumns( Object items, int width, int maxWidth, AttributedStringBuilder sb, Candidate selection, String completed, boolean rowsFirst, boolean doMenuList, int[] out) { if (maxWidth <= 0 || width <= 0) { return; } // This is a group if (items instanceof String) { if (doMenuList) { sb.style(AttributedStyle.DEFAULT); sb.append('\t'); } AttributedStringBuilder asb = new AttributedStringBuilder(); asb.style(getCompletionStyleGroup(doMenuList)) .append((String) items) .style(AttributedStyle.DEFAULT); if (doMenuList) { for (int k = ((String) items).length(); k < maxWidth + 1; k++) { asb.append(' '); } } sb.style(getCompletionStyleBackground(doMenuList)); sb.append(asb); sb.append("\n"); out[0]++; } // This is a Candidate list else if (items instanceof List) { List candidates = (List) items; maxWidth = Math.min(width, maxWidth); int c = width / maxWidth; while (c > 1 && c * maxWidth + (c - 1) * MARGIN_BETWEEN_COLUMNS >= width) { c--; } int lines = (candidates.size() + c - 1) / c; // Try to minimize the number of columns for the given number of rows // Prevents eg 9 candiates being split 6/3 instead of 5/4. final int columns = (candidates.size() + lines - 1) / lines; IntBinaryOperator index; if (rowsFirst) { index = (i, j) -> i * columns + j; } else { index = (i, j) -> j * lines + i; } for (int i = 0; i < lines; i++) { if (doMenuList) { sb.style(AttributedStyle.DEFAULT); sb.append('\t'); } AttributedStringBuilder asb = new AttributedStringBuilder(); for (int j = 0; j < columns; j++) { int idx = index.applyAsInt(i, j); if (idx < candidates.size()) { Candidate cand = candidates.get(idx); boolean hasRightItem = j < columns - 1 && index.applyAsInt(i, j + 1) < candidates.size(); AttributedString left = fromAnsi(cand.displ()); AttributedString right = fromAnsi(cand.descr()); int lw = left.columnLength(); int rw = 0; if (right != null) { int rem = maxWidth - (lw + MARGIN_BETWEEN_DISPLAY_AND_DESC + DESC_PREFIX.length() + DESC_SUFFIX.length()); rw = right.columnLength(); if (rw > rem) { right = AttributedStringBuilder.append( right.columnSubSequence(0, rem - WCWidth.wcwidth('…')), "…"); rw = right.columnLength(); } right = AttributedStringBuilder.append(DESC_PREFIX, right, DESC_SUFFIX); rw += DESC_PREFIX.length() + DESC_SUFFIX.length(); } if (cand == selection) { out[1] = i; asb.style(getCompletionStyleSelection(doMenuList)); if (left.toString() .regionMatches( isSet(Option.CASE_INSENSITIVE), 0, completed, 0, completed.length())) { asb.append(left.toString(), 0, completed.length()); asb.append(left.toString(), completed.length(), left.length()); } else { asb.append(left.toString()); } for (int k = 0; k < maxWidth - lw - rw; k++) { asb.append(' '); } if (right != null) { asb.append(right); } asb.style(AttributedStyle.DEFAULT); } else { if (left.toString() .regionMatches( isSet(Option.CASE_INSENSITIVE), 0, completed, 0, completed.length())) { asb.style(getCompletionStyleStarting(doMenuList)); asb.append(left, 0, completed.length()); asb.style(AttributedStyle.DEFAULT); asb.append(left, completed.length(), left.length()); } else { asb.append(left); } if (right != null || hasRightItem) { for (int k = 0; k < maxWidth - lw - rw; k++) { asb.append(' '); } } if (right != null) { asb.style(getCompletionStyleDescription(doMenuList)); asb.append(right); asb.style(AttributedStyle.DEFAULT); } else if (doMenuList) { for (int k = lw; k < maxWidth; k++) { asb.append(' '); } } } if (hasRightItem) { for (int k = 0; k < MARGIN_BETWEEN_COLUMNS; k++) { asb.append(' '); } } if (doMenuList) { asb.append(' '); } } } sb.style(getCompletionStyleBackground(doMenuList)); sb.append(asb); sb.append('\n'); } out[0] += lines; } } protected AttributedStyle getCompletionStyleStarting(boolean menuList) { return menuList ? getCompletionStyleListStarting() : getCompletionStyleStarting(); } protected AttributedStyle getCompletionStyleDescription(boolean menuList) { return menuList ? getCompletionStyleListDescription() : getCompletionStyleDescription(); } protected AttributedStyle getCompletionStyleGroup(boolean menuList) { return menuList ? getCompletionStyleListGroup() : getCompletionStyleGroup(); } protected AttributedStyle getCompletionStyleSelection(boolean menuList) { return menuList ? getCompletionStyleListSelection() : getCompletionStyleSelection(); } protected AttributedStyle getCompletionStyleBackground(boolean menuList) { return menuList ? getCompletionStyleListBackground() : getCompletionStyleBackground(); } protected AttributedStyle getCompletionStyleStarting() { return getCompletionStyle(COMPLETION_STYLE_STARTING, DEFAULT_COMPLETION_STYLE_STARTING); } protected AttributedStyle getCompletionStyleDescription() { return getCompletionStyle(COMPLETION_STYLE_DESCRIPTION, DEFAULT_COMPLETION_STYLE_DESCRIPTION); } protected AttributedStyle getCompletionStyleGroup() { return getCompletionStyle(COMPLETION_STYLE_GROUP, DEFAULT_COMPLETION_STYLE_GROUP); } protected AttributedStyle getCompletionStyleSelection() { return getCompletionStyle(COMPLETION_STYLE_SELECTION, DEFAULT_COMPLETION_STYLE_SELECTION); } protected AttributedStyle getCompletionStyleBackground() { return getCompletionStyle(COMPLETION_STYLE_BACKGROUND, DEFAULT_COMPLETION_STYLE_BACKGROUND); } protected AttributedStyle getCompletionStyleListStarting() { return getCompletionStyle(COMPLETION_STYLE_LIST_STARTING, DEFAULT_COMPLETION_STYLE_LIST_STARTING); } protected AttributedStyle getCompletionStyleListDescription() { return getCompletionStyle(COMPLETION_STYLE_LIST_DESCRIPTION, DEFAULT_COMPLETION_STYLE_LIST_DESCRIPTION); } protected AttributedStyle getCompletionStyleListGroup() { return getCompletionStyle(COMPLETION_STYLE_LIST_GROUP, DEFAULT_COMPLETION_STYLE_LIST_GROUP); } protected AttributedStyle getCompletionStyleListSelection() { return getCompletionStyle(COMPLETION_STYLE_LIST_SELECTION, DEFAULT_COMPLETION_STYLE_LIST_SELECTION); } protected AttributedStyle getCompletionStyleListBackground() { return getCompletionStyle(COMPLETION_STYLE_LIST_BACKGROUND, DEFAULT_COMPLETION_STYLE_LIST_BACKGROUND); } protected AttributedStyle getCompletionStyle(String name, String value) { return new StyleResolver(s -> getString(s, null)).resolve("." + name, value); } protected AttributedStyle buildStyle(String str) { return fromAnsi("\u001b[" + str + "m ").styleAt(0); } /** * 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 */ protected boolean moveHistory(final boolean next, int count) { boolean ok = true; for (int i = 0; i < count && (ok = moveHistory(next)); i++) { /* empty */ } return ok; } /** * Move up or down the history tree. * @param next true to go to the next, false for the previous. * @return true if successful, false otherwise */ protected boolean moveHistory(final boolean next) { if (!buf.toString().equals(history.current())) { modifiedHistory.put(history.index(), buf.toString()); } if (next && !history.next()) { return false; } else if (!next && !history.previous()) { return false; } setBuffer( modifiedHistory.containsKey(history.index()) ? modifiedHistory.get(history.index()) : history.current()); return true; } // // Printing // /** * Raw output printing. * @param str the string to print to the terminal */ void print(String str) { terminal.writer().write(str); } void println(String s) { print(s); println(); } /** * Output a platform-dependant newline. */ void println() { terminal.puts(Capability.carriage_return); print("\n"); redrawLine(); } // // Actions // protected boolean killBuffer() { killRing.add(buf.toString()); buf.clear(); return true; } protected boolean killWholeLine() { if (buf.length() == 0) { return false; } int start; int end; if (count < 0) { end = buf.cursor(); while (buf.atChar(end) != 0 && buf.atChar(end) != '\n') { end++; } start = end; for (int count = -this.count; count > 0; --count) { while (start > 0 && buf.atChar(start - 1) != '\n') { start--; } start--; } } else { start = buf.cursor(); while (start > 0 && buf.atChar(start - 1) != '\n') { start--; } end = start; while (count-- > 0) { while (end < buf.length() && buf.atChar(end) != '\n') { end++; } if (end < buf.length()) { end++; } } } String killed = buf.substring(start, end); buf.cursor(start); buf.delete(end - start); killRing.add(killed); return true; } /** * Kill the buffer ahead of the current cursor position. * * @return true if successful */ public boolean killLine() { if (count < 0) { return callNeg(this::backwardKillLine); } if (buf.cursor() == buf.length()) { return false; } int cp = buf.cursor(); int len = cp; while (count-- > 0) { if (buf.atChar(len) == '\n') { len++; } else { while (buf.atChar(len) != 0 && buf.atChar(len) != '\n') { len++; } } } int num = len - cp; String killed = buf.substring(cp, cp + num); buf.delete(num); killRing.add(killed); return true; } public boolean backwardKillLine() { if (count < 0) { return callNeg(this::killLine); } if (buf.cursor() == 0) { return false; } int cp = buf.cursor(); int beg = cp; while (count-- > 0) { if (beg == 0) { break; } if (buf.atChar(beg - 1) == '\n') { beg--; } else { while (beg > 0 && buf.atChar(beg - 1) != 0 && buf.atChar(beg - 1) != '\n') { beg--; } } } int num = cp - beg; String killed = buf.substring(cp - beg, cp); buf.cursor(beg); buf.delete(num); killRing.add(killed); return true; } public boolean killRegion() { return doCopyKillRegion(true); } public boolean copyRegionAsKill() { return doCopyKillRegion(false); } private boolean doCopyKillRegion(boolean kill) { if (regionMark > buf.length()) { regionMark = buf.length(); } if (regionActive == RegionType.LINE) { int start = regionMark; int end = buf.cursor(); if (start < end) { while (start > 0 && buf.atChar(start - 1) != '\n') { start--; } while (end < buf.length() - 1 && buf.atChar(end + 1) != '\n') { end++; } if (isInViCmdMode()) { end++; } killRing.add(buf.substring(start, end)); if (kill) { buf.backspace(end - start); } } else { while (end > 0 && buf.atChar(end - 1) != '\n') { end--; } while (start < buf.length() && buf.atChar(start) != '\n') { start++; } if (isInViCmdMode()) { start++; } killRing.addBackwards(buf.substring(end, start)); if (kill) { buf.cursor(end); buf.delete(start - end); } } } else if (regionMark > buf.cursor()) { if (isInViCmdMode()) { regionMark++; } killRing.add(buf.substring(buf.cursor(), regionMark)); if (kill) { buf.delete(regionMark - buf.cursor()); } } else { if (isInViCmdMode()) { buf.move(1); } killRing.add(buf.substring(regionMark, buf.cursor())); if (kill) { buf.backspace(buf.cursor() - regionMark); } } if (kill) { regionActive = RegionType.NONE; } return true; } public boolean yank() { String yanked = killRing.yank(); if (yanked == null) { return false; } else { putString(yanked); return true; } } public boolean yankPop() { if (!killRing.lastYank()) { return false; } String current = killRing.yank(); if (current == null) { // This shouldn't happen. return false; } buf.backspace(current.length()); String yanked = killRing.yankPop(); if (yanked == null) { // This shouldn't happen. return false; } putString(yanked); return true; } public boolean mouse() { MouseEvent event = readMouseEvent(); if (event.getType() == MouseEvent.Type.Released && event.getButton() == MouseEvent.Button.Button1) { StringBuilder tsb = new StringBuilder(); Cursor cursor = terminal.getCursorPosition(c -> tsb.append((char) c)); bindingReader.runMacro(tsb.toString()); List secondaryPrompts = new ArrayList<>(); getDisplayedBufferWithPrompts(secondaryPrompts); AttributedStringBuilder sb = new AttributedStringBuilder().tabs(TAB_WIDTH); sb.append(prompt); sb.append(insertSecondaryPrompts(new AttributedString(buf.upToCursor()), secondaryPrompts, false)); List promptLines = sb.columnSplitLength(size.getColumns(), false, display.delayLineWrap()); int currentLine = promptLines.size() - 1; int wantedLine = Math.max(0, Math.min(currentLine + event.getY() - cursor.getY(), secondaryPrompts.size())); int pl0 = currentLine == 0 ? prompt.columnLength() : secondaryPrompts.get(currentLine - 1).columnLength(); int pl1 = wantedLine == 0 ? prompt.columnLength() : secondaryPrompts.get(wantedLine - 1).columnLength(); int adjust = pl1 - pl0; buf.moveXY(event.getX() - cursor.getX() - adjust, event.getY() - cursor.getY()); } return true; } public boolean beginPaste() { String str = doReadStringUntil(BRACKETED_PASTE_END); regionActive = RegionType.PASTE; regionMark = getBuffer().cursor(); getBuffer().write(str.replace('\r', '\n')); return true; } public boolean focusIn() { return false; } public boolean focusOut() { return false; } /** * Clean the used display * @return true */ public boolean clear() { display.update(Collections.emptyList(), 0); return true; } /** * Clear the screen by issuing the ANSI "clear screen" code. * @return true */ public boolean clearScreen() { if (terminal.puts(Capability.clear_screen)) { // ConEMU extended fonts support if (AbstractWindowsTerminal.TYPE_WINDOWS_CONEMU.equals(terminal.getType()) && !Boolean.getBoolean("org.pkl.thirdparty.jline.terminal.conemu.disable-activate")) { terminal.writer().write("\u001b[9999E"); } Status status = Status.getStatus(terminal, false); if (status != null) { status.reset(); } redrawLine(); } else { println(); } return true; } /** * Issue an audible keyboard bell. * @return true */ public boolean beep() { BellType bell_preference = BellType.AUDIBLE; switch (getString(BELL_STYLE, DEFAULT_BELL_STYLE).toLowerCase()) { case "none": case "off": bell_preference = BellType.NONE; break; case "audible": bell_preference = BellType.AUDIBLE; break; case "visible": bell_preference = BellType.VISIBLE; break; case "on": bell_preference = getBoolean(PREFER_VISIBLE_BELL, false) ? BellType.VISIBLE : BellType.AUDIBLE; break; } if (bell_preference == BellType.VISIBLE) { if (terminal.puts(Capability.flash_screen) || terminal.puts(Capability.bell)) { flush(); } } else if (bell_preference == BellType.AUDIBLE) { if (terminal.puts(Capability.bell)) { flush(); } } return true; } // // 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 */ protected boolean isDelimiter(int 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 */ protected boolean isWhitespace(int c) { return Character.isWhitespace(c); } protected boolean isViAlphaNum(int c) { return c == '_' || Character.isLetterOrDigit(c); } protected boolean isAlpha(int c) { return Character.isLetter(c); } protected boolean isWord(int c) { String wordchars = getString(WORDCHARS, DEFAULT_WORDCHARS); return Character.isLetterOrDigit(c) || (c < 128 && wordchars.indexOf((char) c) >= 0); } String getString(String name, String def) { return ReaderUtils.getString(this, name, def); } boolean getBoolean(String name, boolean def) { return ReaderUtils.getBoolean(this, name, def); } int getInt(String name, int def) { return ReaderUtils.getInt(this, name, def); } long getLong(String name, long def) { return ReaderUtils.getLong(this, name, def); } @Override public Map> defaultKeyMaps() { Map> keyMaps = new HashMap<>(); keyMaps.put(EMACS, emacs()); keyMaps.put(VICMD, viCmd()); keyMaps.put(VIINS, viInsertion()); keyMaps.put(MENU, menu()); keyMaps.put(VIOPP, viOpp()); keyMaps.put(VISUAL, visual()); keyMaps.put(SAFE, safe()); if (getBoolean(BIND_TTY_SPECIAL_CHARS, true)) { Attributes attr = terminal.getAttributes(); bindConsoleChars(keyMaps.get(EMACS), attr); bindConsoleChars(keyMaps.get(VIINS), attr); } // Put default for (KeyMap keyMap : keyMaps.values()) { keyMap.setUnicode(new Reference(SELF_INSERT)); keyMap.setAmbiguousTimeout(getLong(AMBIGUOUS_BINDING, DEFAULT_AMBIGUOUS_BINDING)); } // By default, link main to emacs keyMaps.put(MAIN, keyMaps.get(EMACS)); return keyMaps; } public KeyMap emacs() { KeyMap emacs = new KeyMap<>(); bindKeys(emacs); bind(emacs, SET_MARK_COMMAND, ctrl('@')); bind(emacs, BEGINNING_OF_LINE, ctrl('A')); bind(emacs, BACKWARD_CHAR, ctrl('B')); bind(emacs, DELETE_CHAR_OR_LIST, ctrl('D')); bind(emacs, END_OF_LINE, ctrl('E')); bind(emacs, FORWARD_CHAR, ctrl('F')); bind(emacs, SEND_BREAK, ctrl('G')); bind(emacs, BACKWARD_DELETE_CHAR, ctrl('H')); bind(emacs, EXPAND_OR_COMPLETE, ctrl('I')); bind(emacs, ACCEPT_LINE, ctrl('J')); bind(emacs, KILL_LINE, ctrl('K')); bind(emacs, CLEAR_SCREEN, ctrl('L')); bind(emacs, ACCEPT_LINE, ctrl('M')); bind(emacs, DOWN_LINE_OR_HISTORY, ctrl('N')); bind(emacs, ACCEPT_LINE_AND_DOWN_HISTORY, ctrl('O')); bind(emacs, UP_LINE_OR_HISTORY, ctrl('P')); bind(emacs, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); bind(emacs, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S')); bind(emacs, TRANSPOSE_CHARS, ctrl('T')); bind(emacs, KILL_WHOLE_LINE, ctrl('U')); bind(emacs, QUOTED_INSERT, ctrl('V')); bind(emacs, BACKWARD_KILL_WORD, ctrl('W')); bind(emacs, YANK, ctrl('Y')); bind(emacs, CHARACTER_SEARCH, ctrl(']')); bind(emacs, UNDO, ctrl('_')); bind(emacs, SELF_INSERT, range(" -~")); bind(emacs, INSERT_CLOSE_PAREN, ")"); bind(emacs, INSERT_CLOSE_SQUARE, "]"); bind(emacs, INSERT_CLOSE_CURLY, "}"); bind(emacs, BACKWARD_DELETE_CHAR, del()); bind(emacs, VI_MATCH_BRACKET, translate("^X^B")); bind(emacs, SEND_BREAK, translate("^X^G")); bind(emacs, EDIT_AND_EXECUTE_COMMAND, translate("^X^E")); bind(emacs, VI_FIND_NEXT_CHAR, translate("^X^F")); bind(emacs, VI_JOIN, translate("^X^J")); bind(emacs, KILL_BUFFER, translate("^X^K")); bind(emacs, INFER_NEXT_HISTORY, translate("^X^N")); bind(emacs, OVERWRITE_MODE, translate("^X^O")); bind(emacs, REDO, translate("^X^R")); bind(emacs, UNDO, translate("^X^U")); bind(emacs, VI_CMD_MODE, translate("^X^V")); bind(emacs, EXCHANGE_POINT_AND_MARK, translate("^X^X")); bind(emacs, DO_LOWERCASE_VERSION, translate("^XA-^XZ")); bind(emacs, WHAT_CURSOR_POSITION, translate("^X=")); bind(emacs, KILL_LINE, translate("^X^?")); bind(emacs, SEND_BREAK, alt(ctrl('G'))); bind(emacs, BACKWARD_KILL_WORD, alt(ctrl('H'))); bind(emacs, SELF_INSERT_UNMETA, alt(ctrl('M'))); bind(emacs, COMPLETE_WORD, alt(esc())); bind(emacs, CHARACTER_SEARCH_BACKWARD, alt(ctrl(']'))); bind(emacs, COPY_PREV_WORD, alt(ctrl('_'))); bind(emacs, SET_MARK_COMMAND, alt(' ')); bind(emacs, NEG_ARGUMENT, alt('-')); bind(emacs, DIGIT_ARGUMENT, range("\\E0-\\E9")); bind(emacs, BEGINNING_OF_HISTORY, alt('<')); bind(emacs, LIST_CHOICES, alt('=')); bind(emacs, END_OF_HISTORY, alt('>')); bind(emacs, LIST_CHOICES, alt('?')); bind(emacs, DO_LOWERCASE_VERSION, range("^[A-^[Z")); bind(emacs, ACCEPT_AND_HOLD, alt('a')); bind(emacs, BACKWARD_WORD, alt('b')); bind(emacs, CAPITALIZE_WORD, alt('c')); bind(emacs, KILL_WORD, alt('d')); bind(emacs, KILL_WORD, translate("^[[3;5~")); // ctrl-delete bind(emacs, FORWARD_WORD, alt('f')); bind(emacs, DOWN_CASE_WORD, alt('l')); bind(emacs, HISTORY_SEARCH_FORWARD, alt('n')); bind(emacs, HISTORY_SEARCH_BACKWARD, alt('p')); bind(emacs, TRANSPOSE_WORDS, alt('t')); bind(emacs, UP_CASE_WORD, alt('u')); bind(emacs, YANK_POP, alt('y')); bind(emacs, BACKWARD_KILL_WORD, alt(del())); bindArrowKeys(emacs); bind(emacs, FORWARD_WORD, translate("^[[1;5C")); // ctrl-left bind(emacs, BACKWARD_WORD, translate("^[[1;5D")); // ctrl-right bind(emacs, FORWARD_WORD, alt(key(Capability.key_right))); bind(emacs, BACKWARD_WORD, alt(key(Capability.key_left))); bind(emacs, FORWARD_WORD, alt(translate("^[[C"))); bind(emacs, BACKWARD_WORD, alt(translate("^[[D"))); return emacs; } public KeyMap viInsertion() { KeyMap viins = new KeyMap<>(); bindKeys(viins); bind(viins, SELF_INSERT, range("^@-^_")); bind(viins, LIST_CHOICES, ctrl('D')); bind(viins, SEND_BREAK, ctrl('G')); bind(viins, BACKWARD_DELETE_CHAR, ctrl('H')); bind(viins, EXPAND_OR_COMPLETE, ctrl('I')); bind(viins, ACCEPT_LINE, ctrl('J')); bind(viins, CLEAR_SCREEN, ctrl('L')); bind(viins, ACCEPT_LINE, ctrl('M')); bind(viins, MENU_COMPLETE, ctrl('N')); bind(viins, REVERSE_MENU_COMPLETE, ctrl('P')); bind(viins, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); bind(viins, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S')); bind(viins, TRANSPOSE_CHARS, ctrl('T')); bind(viins, KILL_WHOLE_LINE, ctrl('U')); bind(viins, QUOTED_INSERT, ctrl('V')); bind(viins, BACKWARD_KILL_WORD, ctrl('W')); bind(viins, YANK, ctrl('Y')); bind(viins, VI_CMD_MODE, ctrl('[')); bind(viins, UNDO, ctrl('_')); bind(viins, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('X') + "r"); bind(viins, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('X') + "s"); bind(viins, SELF_INSERT, range(" -~")); bind(viins, INSERT_CLOSE_PAREN, ")"); bind(viins, INSERT_CLOSE_SQUARE, "]"); bind(viins, INSERT_CLOSE_CURLY, "}"); bind(viins, BACKWARD_DELETE_CHAR, del()); bindArrowKeys(viins); return viins; } public KeyMap viCmd() { KeyMap vicmd = new KeyMap<>(); bind(vicmd, LIST_CHOICES, ctrl('D')); bind(vicmd, EMACS_EDITING_MODE, ctrl('E')); bind(vicmd, SEND_BREAK, ctrl('G')); bind(vicmd, VI_BACKWARD_CHAR, ctrl('H')); bind(vicmd, ACCEPT_LINE, ctrl('J')); bind(vicmd, KILL_LINE, ctrl('K')); bind(vicmd, CLEAR_SCREEN, ctrl('L')); bind(vicmd, ACCEPT_LINE, ctrl('M')); bind(vicmd, VI_DOWN_LINE_OR_HISTORY, ctrl('N')); bind(vicmd, VI_UP_LINE_OR_HISTORY, ctrl('P')); bind(vicmd, QUOTED_INSERT, ctrl('Q')); bind(vicmd, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); bind(vicmd, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S')); bind(vicmd, TRANSPOSE_CHARS, ctrl('T')); bind(vicmd, KILL_WHOLE_LINE, ctrl('U')); bind(vicmd, QUOTED_INSERT, ctrl('V')); bind(vicmd, BACKWARD_KILL_WORD, ctrl('W')); bind(vicmd, YANK, ctrl('Y')); bind(vicmd, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('X') + "r"); bind(vicmd, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('X') + "s"); bind(vicmd, SEND_BREAK, alt(ctrl('G'))); bind(vicmd, BACKWARD_KILL_WORD, alt(ctrl('H'))); bind(vicmd, SELF_INSERT_UNMETA, alt(ctrl('M'))); bind(vicmd, COMPLETE_WORD, alt(esc())); bind(vicmd, CHARACTER_SEARCH_BACKWARD, alt(ctrl(']'))); bind(vicmd, SET_MARK_COMMAND, alt(' ')); // bind(vicmd, INSERT_COMMENT, alt('#')); // bind(vicmd, INSERT_COMPLETIONS, alt('*')); bind(vicmd, DIGIT_ARGUMENT, alt('-')); bind(vicmd, BEGINNING_OF_HISTORY, alt('<')); bind(vicmd, LIST_CHOICES, alt('=')); bind(vicmd, END_OF_HISTORY, alt('>')); bind(vicmd, LIST_CHOICES, alt('?')); bind(vicmd, DO_LOWERCASE_VERSION, range("^[A-^[Z")); bind(vicmd, BACKWARD_WORD, alt('b')); bind(vicmd, CAPITALIZE_WORD, alt('c')); bind(vicmd, KILL_WORD, alt('d')); bind(vicmd, FORWARD_WORD, alt('f')); bind(vicmd, DOWN_CASE_WORD, alt('l')); bind(vicmd, HISTORY_SEARCH_FORWARD, alt('n')); bind(vicmd, HISTORY_SEARCH_BACKWARD, alt('p')); bind(vicmd, TRANSPOSE_WORDS, alt('t')); bind(vicmd, UP_CASE_WORD, alt('u')); bind(vicmd, YANK_POP, alt('y')); bind(vicmd, BACKWARD_KILL_WORD, alt(del())); bind(vicmd, FORWARD_CHAR, " "); bind(vicmd, VI_INSERT_COMMENT, "#"); bind(vicmd, END_OF_LINE, "$"); bind(vicmd, VI_MATCH_BRACKET, "%"); bind(vicmd, VI_DOWN_LINE_OR_HISTORY, "+"); bind(vicmd, VI_REV_REPEAT_FIND, ","); bind(vicmd, VI_UP_LINE_OR_HISTORY, "-"); bind(vicmd, VI_REPEAT_CHANGE, "."); bind(vicmd, VI_HISTORY_SEARCH_BACKWARD, "/"); bind(vicmd, VI_DIGIT_OR_BEGINNING_OF_LINE, "0"); bind(vicmd, DIGIT_ARGUMENT, range("1-9")); bind(vicmd, VI_REPEAT_FIND, ";"); bind(vicmd, LIST_CHOICES, "="); bind(vicmd, VI_HISTORY_SEARCH_FORWARD, "?"); bind(vicmd, VI_ADD_EOL, "A"); bind(vicmd, VI_BACKWARD_BLANK_WORD, "B"); bind(vicmd, VI_CHANGE_EOL, "C"); bind(vicmd, VI_KILL_EOL, "D"); bind(vicmd, VI_FORWARD_BLANK_WORD_END, "E"); bind(vicmd, VI_FIND_PREV_CHAR, "F"); bind(vicmd, VI_FETCH_HISTORY, "G"); bind(vicmd, VI_INSERT_BOL, "I"); bind(vicmd, VI_JOIN, "J"); bind(vicmd, VI_REV_REPEAT_SEARCH, "N"); bind(vicmd, VI_OPEN_LINE_ABOVE, "O"); bind(vicmd, VI_PUT_BEFORE, "P"); bind(vicmd, VI_REPLACE, "R"); bind(vicmd, VI_KILL_LINE, "S"); bind(vicmd, VI_FIND_PREV_CHAR_SKIP, "T"); bind(vicmd, REDO, "U"); bind(vicmd, VISUAL_LINE_MODE, "V"); bind(vicmd, VI_FORWARD_BLANK_WORD, "W"); bind(vicmd, VI_BACKWARD_DELETE_CHAR, "X"); bind(vicmd, VI_YANK_WHOLE_LINE, "Y"); bind(vicmd, VI_FIRST_NON_BLANK, "^"); bind(vicmd, VI_ADD_NEXT, "a"); bind(vicmd, VI_BACKWARD_WORD, "b"); bind(vicmd, VI_CHANGE, "c"); bind(vicmd, VI_DELETE, "d"); bind(vicmd, VI_FORWARD_WORD_END, "e"); bind(vicmd, VI_FIND_NEXT_CHAR, "f"); bind(vicmd, WHAT_CURSOR_POSITION, "ga"); bind(vicmd, VI_BACKWARD_BLANK_WORD_END, "gE"); bind(vicmd, VI_BACKWARD_WORD_END, "ge"); bind(vicmd, VI_BACKWARD_CHAR, "h"); bind(vicmd, VI_INSERT, "i"); bind(vicmd, DOWN_LINE_OR_HISTORY, "j"); bind(vicmd, UP_LINE_OR_HISTORY, "k"); bind(vicmd, VI_FORWARD_CHAR, "l"); bind(vicmd, VI_REPEAT_SEARCH, "n"); bind(vicmd, VI_OPEN_LINE_BELOW, "o"); bind(vicmd, VI_PUT_AFTER, "p"); bind(vicmd, VI_REPLACE_CHARS, "r"); bind(vicmd, VI_SUBSTITUTE, "s"); bind(vicmd, VI_FIND_NEXT_CHAR_SKIP, "t"); bind(vicmd, UNDO, "u"); bind(vicmd, VISUAL_MODE, "v"); bind(vicmd, VI_FORWARD_WORD, "w"); bind(vicmd, VI_DELETE_CHAR, "x"); bind(vicmd, VI_YANK, "y"); bind(vicmd, VI_GOTO_COLUMN, "|"); bind(vicmd, VI_SWAP_CASE, "~"); bind(vicmd, VI_BACKWARD_CHAR, del()); bindArrowKeys(vicmd); return vicmd; } public KeyMap menu() { KeyMap menu = new KeyMap<>(); bind(menu, MENU_COMPLETE, "\t"); bind(menu, REVERSE_MENU_COMPLETE, key(Capability.back_tab)); bind(menu, ACCEPT_LINE, "\r", "\n"); bindArrowKeys(menu); return menu; } public KeyMap safe() { KeyMap safe = new KeyMap<>(); bind(safe, SELF_INSERT, range("^@-^?")); bind(safe, ACCEPT_LINE, "\r", "\n"); bind(safe, SEND_BREAK, ctrl('G')); return safe; } public KeyMap visual() { KeyMap visual = new KeyMap<>(); bind(visual, UP_LINE, key(Capability.key_up), "k"); bind(visual, DOWN_LINE, key(Capability.key_down), "j"); bind(visual, this::deactivateRegion, esc()); bind(visual, EXCHANGE_POINT_AND_MARK, "o"); bind(visual, PUT_REPLACE_SELECTION, "p"); bind(visual, VI_DELETE, "x"); bind(visual, VI_OPER_SWAP_CASE, "~"); return visual; } public KeyMap viOpp() { KeyMap viOpp = new KeyMap<>(); bind(viOpp, UP_LINE, key(Capability.key_up), "k"); bind(viOpp, DOWN_LINE, key(Capability.key_down), "j"); bind(viOpp, VI_CMD_MODE, esc()); return viOpp; } private void bind(KeyMap map, String widget, Iterable keySeqs) { map.bind(new Reference(widget), keySeqs); } private void bind(KeyMap map, String widget, CharSequence... keySeqs) { map.bind(new Reference(widget), keySeqs); } private void bind(KeyMap map, Widget widget, CharSequence... keySeqs) { map.bind(widget, keySeqs); } private String key(Capability capability) { return KeyMap.key(terminal, capability); } private void bindKeys(KeyMap emacs) { Widget beep = namedWidget("beep", this::beep); Stream.of(Capability.values()) .filter(c -> c.name().startsWith("key_")) .map(this::key) .forEach(k -> bind(emacs, beep, k)); } private void bindArrowKeys(KeyMap map) { bind(map, UP_LINE_OR_SEARCH, key(Capability.key_up)); bind(map, DOWN_LINE_OR_SEARCH, key(Capability.key_down)); bind(map, BACKWARD_CHAR, key(Capability.key_left)); bind(map, FORWARD_CHAR, key(Capability.key_right)); bind(map, BEGINNING_OF_LINE, key(Capability.key_home)); bind(map, END_OF_LINE, key(Capability.key_end)); bind(map, DELETE_CHAR, key(Capability.key_dc)); bind(map, KILL_WHOLE_LINE, key(Capability.key_dl)); bind(map, OVERWRITE_MODE, key(Capability.key_ic)); bind(map, MOUSE, key(Capability.key_mouse)); bind(map, BEGIN_PASTE, BRACKETED_PASTE_BEGIN); bind(map, FOCUS_IN, FOCUS_IN_SEQ); bind(map, FOCUS_OUT, FOCUS_OUT_SEQ); } /** * Bind special chars defined by the terminal instead of * the default bindings */ private void bindConsoleChars(KeyMap keyMap, Attributes attr) { if (attr != null) { rebind(keyMap, BACKWARD_DELETE_CHAR, del(), (char) attr.getControlChar(ControlChar.VERASE)); rebind(keyMap, BACKWARD_KILL_WORD, ctrl('W'), (char) attr.getControlChar(ControlChar.VWERASE)); rebind(keyMap, KILL_WHOLE_LINE, ctrl('U'), (char) attr.getControlChar(ControlChar.VKILL)); rebind(keyMap, QUOTED_INSERT, ctrl('V'), (char) attr.getControlChar(ControlChar.VLNEXT)); } } private void rebind(KeyMap keyMap, String operation, String prevBinding, char newBinding) { if (newBinding > 0 && newBinding < 128) { Reference ref = new Reference(operation); bind(keyMap, SELF_INSERT, prevBinding); keyMap.bind(ref, Character.toString(newBinding)); } } }