// 
// Decompiled by Procyon v0.6.0
// 

package org.jline.reader.impl;

import org.jline.terminal.impl.MouseSupport;
import java.util.stream.Stream;
import org.jline.terminal.Cursor;
import org.jline.utils.StyleResolver;
import java.util.function.IntBinaryOperator;
import org.jline.utils.AttributedCharSequence;
import java.util.HashSet;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.LinkedHashMap;
import java.util.TreeMap;
import java.util.function.BiFunction;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.function.ToIntFunction;
import org.jline.reader.CompletingParsedLine;
import org.jline.reader.Candidate;
import org.jline.utils.WCWidth;
import java.io.FileWriter;
import java.io.File;
import java.util.ListIterator;
import java.util.regex.Matcher;
import java.util.stream.StreamSupport;
import java.util.Spliterators;
import java.util.function.Function;
import java.util.Comparator;
import java.util.regex.Pattern;
import org.jline.reader.Macro;
import org.jline.utils.Status;
import java.time.Instant;
import java.io.BufferedReader;
import java.lang.reflect.Constructor;
import org.jline.reader.Editor;
import java.util.Collection;
import org.jline.reader.Reference;
import java.io.IOError;
import java.io.InterruptedIOException;
import org.jline.terminal.Attributes;
import org.jline.utils.AttributedStyle;
import org.jline.utils.AttributedStringBuilder;
import org.jline.reader.SyntaxError;
import org.jline.reader.EOFError;
import org.jline.reader.EndOfFileException;
import org.jline.reader.UserInterruptException;
import org.jline.terminal.MouseEvent;
import java.util.Collections;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Properties;
import org.jline.utils.Log;
import java.nio.file.OpenOption;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Paths;
import org.jline.utils.Curses;
import org.jline.utils.InfoCmp;
import java.util.Objects;
import java.util.ArrayList;
import java.util.HashMap;
import org.jline.reader.impl.history.DefaultHistory;
import java.io.IOException;
import java.util.List;
import org.jline.utils.Display;
import org.jline.reader.ParsedLine;
import org.jline.reader.Widget;
import java.util.function.Supplier;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import org.jline.keymap.BindingReader;
import org.jline.reader.MaskingCallback;
import org.jline.utils.AttributedString;
import org.jline.terminal.Size;
import org.jline.reader.Buffer;
import org.jline.reader.CompletionMatcher;
import org.jline.reader.Expander;
import org.jline.reader.Parser;
import org.jline.reader.Highlighter;
import org.jline.reader.Completer;
import org.jline.reader.History;
import org.jline.reader.Binding;
import org.jline.keymap.KeyMap;
import java.util.Map;
import org.jline.terminal.Terminal;
import java.io.Flushable;
import org.jline.reader.LineReader;

public class LineReaderImpl implements LineReader, Flushable
{
    public static final char NULL_MASK = '\0';
    @Deprecated
    public static final int TAB_WIDTH = 4;
    public static final int DEFAULT_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 = "\u001b\n";
    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 = "fg:cyan";
    public static final String DEFAULT_COMPLETION_STYLE_LIST_DESCRIPTION = "fg:bright-black";
    public static final String DEFAULT_COMPLETION_STYLE_LIST_GROUP = "fg:black,bold";
    public static final String DEFAULT_COMPLETION_STYLE_LIST_SELECTION = "inverse";
    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;
    public static final String DEFAULT_SYSTEM_PROPERTY_PREFIX = "org.jline.reader.props.";
    private static final int MIN_ROWS = 3;
    public static final String BRACKETED_PASTE_ON = "\u001b[?2004h";
    public static final String BRACKETED_PASTE_OFF = "\u001b[?2004l";
    public static final String BRACKETED_PASTE_BEGIN = "\u001b[200~";
    public static final String BRACKETED_PASTE_END = "\u001b[201~";
    public static final String FOCUS_IN_SEQ = "\u001b[I";
    public static final String FOCUS_OUT_SEQ = "\u001b[O";
    public static final int DEFAULT_MAX_REPEAT_COUNT = 9999;
    protected final Terminal terminal;
    protected final String appName;
    protected final Map<String, KeyMap<Binding>> keyMaps;
    protected final Map<String, Object> variables;
    protected History history;
    protected Completer completer;
    protected Highlighter highlighter;
    protected Parser parser;
    protected Expander expander;
    protected CompletionMatcher completionMatcher;
    protected final Map<Option, Boolean> options;
    protected Thread maskThread;
    protected final Buffer buf;
    protected String tailTip;
    protected SuggestionType autosuggestion;
    protected final Size size;
    protected AttributedString prompt;
    protected AttributedString rightPrompt;
    protected MaskingCallback maskingCallback;
    protected Map<Integer, String> modifiedHistory;
    protected Buffer historyBuffer;
    protected CharSequence searchBuffer;
    protected StringBuffer searchTerm;
    protected boolean searchFailing;
    protected boolean searchBackward;
    protected int searchIndex;
    protected boolean doAutosuggestion;
    protected final BindingReader bindingReader;
    protected int findChar;
    protected int findDir;
    protected int findTailAdd;
    private int searchDir;
    private String searchString;
    protected int regionMark;
    protected RegionType regionActive;
    private boolean forceChar;
    private boolean forceLine;
    protected String yankBuffer;
    protected ViMoveMode viMoveMode;
    protected KillRing killRing;
    protected UndoTree<Buffer> undo;
    protected boolean isUndo;
    protected final ReentrantLock lock;
    protected State state;
    protected final AtomicBoolean startedReading;
    protected boolean reading;
    protected Supplier<AttributedString> post;
    protected Map<String, Widget> builtinWidgets;
    protected Map<String, Widget> widgets;
    protected int count;
    protected int mult;
    protected int universal;
    protected int repeatCount;
    protected boolean isArgDigit;
    protected ParsedLine parsedLine;
    protected boolean skipRedisplay;
    protected Display display;
    protected boolean overTyping;
    protected String keyMap;
    protected int smallTerminalOffset;
    protected boolean nextCommandFromHistory;
    protected int nextHistoryId;
    protected List<String> commandsBuffer;
    protected int candidateStartPosition;
    protected String alternateIn;
    protected String alternateOut;
    protected int currentLine;
    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;
    
    public LineReaderImpl(final Terminal terminal) throws IOException {
        this(terminal, terminal.getName(), null);
    }
    
    public LineReaderImpl(final Terminal terminal, final String appName) throws IOException {
        this(terminal, appName, null);
    }
    
    public LineReaderImpl(final Terminal terminal, String appName, final Map<String, Object> variables) {
        this.history = new DefaultHistory();
        this.completer = null;
        this.highlighter = new DefaultHighlighter();
        this.parser = new DefaultParser();
        this.expander = new DefaultExpander();
        this.completionMatcher = new CompletionMatcherImpl();
        this.options = new HashMap<Option, Boolean>();
        this.maskThread = null;
        this.buf = new BufferImpl();
        this.tailTip = "";
        this.autosuggestion = SuggestionType.NONE;
        this.size = new Size();
        this.prompt = AttributedString.EMPTY;
        this.rightPrompt = AttributedString.EMPTY;
        this.modifiedHistory = new HashMap<Integer, String>();
        this.historyBuffer = null;
        this.searchTerm = null;
        this.searchIndex = -1;
        this.yankBuffer = "";
        this.viMoveMode = ViMoveMode.NORMAL;
        this.killRing = new KillRing();
        this.lock = new ReentrantLock();
        this.state = State.DONE;
        this.startedReading = new AtomicBoolean();
        this.universal = 4;
        this.overTyping = false;
        this.smallTerminalOffset = 0;
        this.nextCommandFromHistory = false;
        this.nextHistoryId = -1;
        this.commandsBuffer = new ArrayList<String>();
        this.candidateStartPosition = 0;
        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<String, Object>();
        }
        final String prefix = this.getString("system-property-prefix", "org.jline.reader.props.");
        if (prefix != null) {
            final Properties sysProps = System.getProperties();
            for (final String s : sysProps.stringPropertyNames()) {
                if (s.startsWith(prefix)) {
                    final String key = s.substring(prefix.length());
                    InputRC.setVar(this, key, sysProps.getProperty(s));
                }
            }
        }
        this.keyMaps = this.defaultKeyMaps();
        if (!Boolean.getBoolean("org.jline.utils.disableAlternateCharset")) {
            this.alternateIn = Curses.tputs(terminal.getStringCapability(InfoCmp.Capability.enter_alt_charset_mode), new Object[0]);
            this.alternateOut = Curses.tputs(terminal.getStringCapability(InfoCmp.Capability.exit_alt_charset_mode), new Object[0]);
        }
        this.undo = new UndoTree<Buffer>(this::setBuffer);
        this.builtinWidgets = this.builtinWidgets();
        this.widgets = new HashMap<String, Widget>(this.builtinWidgets);
        this.bindingReader = new BindingReader(terminal.reader());
        final String inputRc = this.getString("input-rc-file-name", null);
        if (inputRc != null) {
            final Path inputRcPath = Paths.get(inputRc, new String[0]);
            if (Files.exists(inputRcPath, new LinkOption[0])) {
                try (final InputStream is = Files.newInputStream(inputRcPath, new OpenOption[0])) {
                    InputRC.configure(this, is);
                }
                catch (final Exception e) {
                    Log.debug("Error reading inputRc config file: ", inputRc, e);
                }
            }
        }
        this.doDisplay();
    }
    
    @Override
    public Terminal getTerminal() {
        return this.terminal;
    }
    
    @Override
    public String getAppName() {
        return this.appName;
    }
    
    @Override
    public Map<String, KeyMap<Binding>> getKeyMaps() {
        return this.keyMaps;
    }
    
    @Override
    public KeyMap<Binding> getKeys() {
        return this.keyMaps.get(this.keyMap);
    }
    
    @Override
    public Map<String, Widget> getWidgets() {
        return this.widgets;
    }
    
    @Override
    public Map<String, Widget> getBuiltinWidgets() {
        return Collections.unmodifiableMap((Map<? extends String, ? extends Widget>)this.builtinWidgets);
    }
    
    @Override
    public Buffer getBuffer() {
        return this.buf;
    }
    
    @Override
    public void setAutosuggestion(final SuggestionType type) {
        this.autosuggestion = type;
    }
    
    @Override
    public SuggestionType getAutosuggestion() {
        return this.autosuggestion;
    }
    
    @Override
    public String getTailTip() {
        return this.tailTip;
    }
    
    @Override
    public void setTailTip(final String tailTip) {
        this.tailTip = tailTip;
    }
    
    @Override
    public void runMacro(final String macro) {
        this.bindingReader.runMacro(macro);
    }
    
    @Override
    public MouseEvent readMouseEvent() {
        final Terminal terminal = this.terminal;
        final BindingReader bindingReader = this.bindingReader;
        Objects.requireNonNull(bindingReader);
        return terminal.readMouseEvent(bindingReader::readCharacter, this.bindingReader.getLastBinding());
    }
    
    public void setCompleter(final Completer completer) {
        this.completer = completer;
    }
    
    public Completer getCompleter() {
        return this.completer;
    }
    
    public void setHistory(final History history) {
        Objects.requireNonNull(history);
        this.history = history;
    }
    
    @Override
    public History getHistory() {
        return this.history;
    }
    
    public void setHighlighter(final Highlighter highlighter) {
        this.highlighter = highlighter;
    }
    
    @Override
    public Highlighter getHighlighter() {
        return this.highlighter;
    }
    
    @Override
    public Parser getParser() {
        return this.parser;
    }
    
    public void setParser(final Parser parser) {
        this.parser = parser;
    }
    
    @Override
    public Expander getExpander() {
        return this.expander;
    }
    
    public void setExpander(final Expander expander) {
        this.expander = expander;
    }
    
    public void setCompletionMatcher(final CompletionMatcher completionMatcher) {
        this.completionMatcher = completionMatcher;
    }
    
    @Override
    public String readLine() throws UserInterruptException, EndOfFileException {
        return this.readLine(null, null, (MaskingCallback)null, null);
    }
    
    @Override
    public String readLine(final Character mask) throws UserInterruptException, EndOfFileException {
        return this.readLine(null, null, mask, null);
    }
    
    @Override
    public String readLine(final String prompt) throws UserInterruptException, EndOfFileException {
        return this.readLine(prompt, null, (MaskingCallback)null, null);
    }
    
    @Override
    public String readLine(final String prompt, final Character mask) throws UserInterruptException, EndOfFileException {
        return this.readLine(prompt, null, mask, null);
    }
    
    @Override
    public String readLine(final String prompt, final Character mask, final String buffer) throws UserInterruptException, EndOfFileException {
        return this.readLine(prompt, null, mask, buffer);
    }
    
    @Override
    public String readLine(final String prompt, final String rightPrompt, final Character mask, final String buffer) throws UserInterruptException, EndOfFileException {
        return this.readLine(prompt, rightPrompt, (mask != null) ? new SimpleMaskingCallback(mask) : null, buffer);
    }
    
    @Override
    public String readLine(final String prompt, final String rightPrompt, final MaskingCallback maskingCallback, final String buffer) throws UserInterruptException, EndOfFileException {
        if (!this.commandsBuffer.isEmpty()) {
            String cmd = this.commandsBuffer.remove(0);
            boolean done = false;
            do {
                try {
                    this.parser.parse(cmd, cmd.length() + 1, Parser.ParseContext.ACCEPT_LINE);
                    done = true;
                }
                catch (final EOFError e) {
                    if (this.commandsBuffer.isEmpty()) {
                        throw new IllegalArgumentException("Incompleted command: \n" + cmd);
                    }
                    cmd += "\n";
                    cmd += this.commandsBuffer.remove(0);
                }
                catch (final SyntaxError e2) {
                    done = true;
                }
                catch (final Exception e3) {
                    this.commandsBuffer.clear();
                    throw new IllegalArgumentException(e3.getMessage());
                }
            } while (!done);
            final AttributedStringBuilder sb = new AttributedStringBuilder();
            sb.styled(AttributedStyle::bold, cmd);
            sb.toAttributedString().println(this.terminal);
            this.terminal.flush();
            return this.finish(cmd);
        }
        if (!this.startedReading.compareAndSet(false, true)) {
            throw new IllegalStateException();
        }
        final Thread readLineThread = Thread.currentThread();
        Terminal.SignalHandler previousIntrHandler = null;
        Terminal.SignalHandler previousWinchHandler = null;
        Terminal.SignalHandler previousContHandler = null;
        Attributes originalAttributes = null;
        final boolean dumb = this.isTerminalDumb();
        try {
            this.maskingCallback = maskingCallback;
            this.repeatCount = 0;
            this.mult = 1;
            this.regionActive = RegionType.NONE;
            this.regionMark = -1;
            this.smallTerminalOffset = 0;
            this.state = State.NORMAL;
            this.modifiedHistory.clear();
            this.setPrompt(prompt);
            this.setRightPrompt(rightPrompt);
            this.buf.clear();
            if (buffer != null) {
                this.buf.write(buffer);
            }
            if (this.nextCommandFromHistory && this.nextHistoryId > 0) {
                if (this.history.size() > this.nextHistoryId) {
                    this.history.moveTo(this.nextHistoryId);
                }
                else {
                    this.history.moveTo(this.history.last());
                }
                this.buf.write(this.history.current());
            }
            else {
                this.nextHistoryId = -1;
            }
            this.nextCommandFromHistory = false;
            this.undo.clear();
            this.parsedLine = null;
            this.keyMap = "main";
            if (this.history != null) {
                this.history.attach(this);
            }
            try {
                this.lock.lock();
                this.reading = true;
                previousIntrHandler = this.terminal.handle(Terminal.Signal.INT, signal -> readLineThread.interrupt());
                previousWinchHandler = this.terminal.handle(Terminal.Signal.WINCH, this::handleSignal);
                previousContHandler = this.terminal.handle(Terminal.Signal.CONT, this::handleSignal);
                originalAttributes = this.terminal.enterRawMode();
                this.doDisplay();
                if (!dumb) {
                    this.terminal.puts(InfoCmp.Capability.keypad_xmit, new Object[0]);
                    if (this.isSet(Option.AUTO_FRESH_LINE)) {
                        this.callWidget("fresh-line");
                    }
                    if (this.isSet(Option.MOUSE)) {
                        this.terminal.trackMouse(Terminal.MouseTracking.Normal);
                    }
                    if (this.isSet(Option.BRACKETED_PASTE)) {
                        this.terminal.writer().write("\u001b[?2004h");
                    }
                }
                else if (this.isTerminalDumb() && maskingCallback != null) {
                    this.setupMaskThread((prompt != null) ? prompt : "");
                }
                this.callWidget("callback-init");
                if (!this.isSet(Option.DISABLE_UNDO)) {
                    this.undo.newState(this.buf.copy());
                }
                this.redrawLine();
                this.redisplay();
            }
            finally {
                this.lock.unlock();
            }
            while (true) {
                KeyMap<Binding> local = null;
                if (this.isInViCmdMode() && this.regionActive != RegionType.NONE) {
                    local = this.keyMaps.get("visual");
                }
                final Binding o = this.readBinding(this.getKeys(), local);
                if (o == null) {
                    throw new EndOfFileException().partialLine((this.buf.length() > 0) ? this.buf.toString() : null);
                }
                Log.trace("Binding: ", o);
                if (this.buf.length() == 0 && this.getLastBinding().charAt(0) == originalAttributes.getControlChar(Attributes.ControlChar.VEOF)) {
                    throw new EndOfFileException();
                }
                this.isArgDigit = false;
                this.count = ((this.repeatCount == 0) ? 1 : this.repeatCount) * this.mult;
                this.isUndo = false;
                if (this.regionActive == RegionType.PASTE) {
                    this.regionActive = RegionType.NONE;
                }
                try {
                    this.lock.lock();
                    final Buffer copy = (this.buf.length() <= this.getInt("features-max-buffer-size", 1000)) ? this.buf.copy() : null;
                    final Widget w = this.getWidget(o);
                    if (!w.apply()) {
                        this.beep();
                    }
                    if (!this.isSet(Option.DISABLE_UNDO) && !this.isUndo && copy != null && this.buf.length() <= this.getInt("features-max-buffer-size", 1000) && !copy.toString().equals(this.buf.toString())) {
                        this.undo.newState(this.buf.copy());
                    }
                    switch (this.state.ordinal()) {
                        case 1: {
                            return this.finishBuffer();
                        }
                        case 2: {
                            return "";
                        }
                        case 3: {
                            throw new EndOfFileException();
                        }
                        case 4: {
                            throw new UserInterruptException(this.buf.toString());
                        }
                        default: {
                            if (!this.isArgDigit) {
                                this.repeatCount = 0;
                                this.mult = 1;
                            }
                            if (dumb) {
                                continue;
                            }
                            this.redisplay();
                            continue;
                        }
                    }
                }
                finally {
                    this.lock.unlock();
                }
            }
        }
        catch (final IOError e4) {
            if (e4.getCause() instanceof InterruptedIOException) {
                throw new UserInterruptException(this.buf.toString());
            }
            throw e4;
        }
        finally {
            final AtomicBoolean interrupted = new AtomicBoolean(Thread.interrupted());
            try {
                this.lock.lock();
                this.reading = false;
                final Terminal.SignalHandler tmpHandler = this.terminal.handle(Terminal.Signal.INT, s -> interrupted.set(true));
                if (previousIntrHandler == null) {
                    previousIntrHandler = tmpHandler;
                }
                this.cleanup();
                if (originalAttributes != null) {
                    this.terminal.setAttributes(originalAttributes);
                }
                if (previousIntrHandler != null) {
                    this.terminal.handle(Terminal.Signal.INT, previousIntrHandler);
                }
                if (previousWinchHandler != null) {
                    this.terminal.handle(Terminal.Signal.WINCH, previousWinchHandler);
                }
                if (previousContHandler != null) {
                    this.terminal.handle(Terminal.Signal.CONT, previousContHandler);
                }
            }
            finally {
                this.lock.unlock();
                this.startedReading.set(false);
                if (interrupted.get()) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }
    
    private boolean isTerminalDumb() {
        return "dumb".equals(this.terminal.getType()) || "dumb-color".equals(this.terminal.getType());
    }
    
    private void doDisplay() {
        this.size.copy(this.terminal.getBufferSize());
        (this.display = new Display(this.terminal, false)).resize(this.size.getRows(), this.size.getColumns());
        if (this.isSet(Option.DELAY_LINE_WRAP)) {
            this.display.setDelayLineWrap(true);
        }
    }
    
    private void setupMaskThread(final String prompt) {
        if (this.isTerminalDumb() && this.maskThread == null) {
            final String fullPrompt = "\r" + prompt + "                                                   \r" + prompt;
            (this.maskThread = new Thread("JLine Mask Thread") {
                @Override
                public void run() {
                    while (!Thread.interrupted()) {
                        try {
                            LineReaderImpl.this.terminal.writer().write(fullPrompt);
                            LineReaderImpl.this.terminal.writer().flush();
                            Thread.sleep(3L);
                            continue;
                        }
                        catch (final InterruptedException ie) {
                            return;
                        }
                        break;
                    }
                }
            }).setPriority(10);
            this.maskThread.setDaemon(true);
            this.maskThread.start();
        }
    }
    
    private void stopMaskThread() {
        if (this.maskThread != null && this.maskThread.isAlive()) {
            this.maskThread.interrupt();
        }
        this.maskThread = null;
    }
    
    @Override
    public void printAbove(final String str) {
        try {
            this.lock.lock();
            final boolean reading = this.reading;
            if (reading) {
                this.display.update(Collections.emptyList(), 0);
            }
            if (str.endsWith("\n") || str.endsWith("\n\u001b[m") || str.endsWith("\n\u001b[0m")) {
                this.terminal.writer().print(str);
            }
            else {
                this.terminal.writer().println(str);
            }
            if (reading) {
                this.redisplay(false);
            }
            this.terminal.flush();
        }
        finally {
            this.lock.unlock();
        }
    }
    
    @Override
    public void printAbove(final AttributedString str) {
        this.printAbove(str.toAnsi(this.terminal));
    }
    
    @Override
    public boolean isReading() {
        try {
            this.lock.lock();
            return this.reading;
        }
        finally {
            this.lock.unlock();
        }
    }
    
    protected boolean freshLine() {
        final boolean wrapAtEol = this.terminal.getBooleanCapability(InfoCmp.Capability.auto_right_margin);
        final boolean delayedWrapAtEol = wrapAtEol && this.terminal.getBooleanCapability(InfoCmp.Capability.eat_newline_glitch);
        final AttributedStringBuilder sb = new AttributedStringBuilder();
        sb.style(AttributedStyle.DEFAULT.foreground(8));
        sb.append("~");
        sb.style(AttributedStyle.DEFAULT);
        if (!wrapAtEol || delayedWrapAtEol) {
            for (int i = 0; i < this.size.getColumns() - 1; ++i) {
                sb.append(" ");
            }
            sb.append(KeyMap.key(this.terminal, InfoCmp.Capability.carriage_return));
            sb.append(" ");
            sb.append(KeyMap.key(this.terminal, InfoCmp.Capability.carriage_return));
        }
        else {
            final String el = this.terminal.getStringCapability(InfoCmp.Capability.clr_eol);
            if (el != null) {
                Curses.tputs(sb, el, new Object[0]);
            }
            for (int j = 0; j < this.size.getColumns() - 2; ++j) {
                sb.append(" ");
            }
            sb.append(KeyMap.key(this.terminal, InfoCmp.Capability.carriage_return));
            sb.append(" ");
            sb.append(KeyMap.key(this.terminal, InfoCmp.Capability.carriage_return));
        }
        sb.print(this.terminal);
        return true;
    }
    
    @Override
    public void callWidget(final String name) {
        try {
            this.lock.lock();
            if (!this.reading) {
                throw new IllegalStateException("Widgets can only be called during a `readLine` call");
            }
            try {
                Widget w;
                if (name.startsWith(".")) {
                    w = this.builtinWidgets.get(name.substring(1));
                }
                else {
                    w = this.widgets.get(name);
                }
                if (w != null) {
                    w.apply();
                }
            }
            catch (final Throwable t) {
                Log.debug("Error executing widget '", name, "'", t);
            }
        }
        finally {
            this.lock.unlock();
        }
    }
    
    public boolean redrawLine() {
        this.display.reset();
        return true;
    }
    
    public void putString(final CharSequence str) {
        this.buf.write(str, this.overTyping);
    }
    
    @Override
    public void flush() {
        this.terminal.flush();
    }
    
    public boolean isKeyMap(final String name) {
        return this.keyMap.equals(name);
    }
    
    public int readCharacter() {
        if (this.lock.isHeldByCurrentThread()) {
            try {
                this.lock.unlock();
                return this.bindingReader.readCharacter();
            }
            finally {
                this.lock.lock();
            }
        }
        return this.bindingReader.readCharacter();
    }
    
    public int peekCharacter(final long timeout) {
        return this.bindingReader.peekCharacter(timeout);
    }
    
    protected <T> T doReadBinding(final KeyMap<T> keys, final KeyMap<T> local) {
        if (this.lock.isHeldByCurrentThread()) {
            try {
                this.lock.unlock();
                return this.bindingReader.readBinding(keys, local);
            }
            finally {
                this.lock.lock();
            }
        }
        return this.bindingReader.readBinding(keys, local);
    }
    
    protected String doReadStringUntil(final String sequence) {
        if (this.lock.isHeldByCurrentThread()) {
            try {
                this.lock.unlock();
                return this.bindingReader.readStringUntil(sequence);
            }
            finally {
                this.lock.lock();
            }
        }
        return this.bindingReader.readStringUntil(sequence);
    }
    
    public Binding readBinding(final KeyMap<Binding> keys) {
        return this.readBinding(keys, null);
    }
    
    public Binding readBinding(final KeyMap<Binding> keys, final KeyMap<Binding> local) {
        final Binding o = this.doReadBinding(keys, local);
        if (o instanceof Reference) {
            final String ref = ((Reference)o).name();
            if (!"yank-pop".equals(ref) && !"yank".equals(ref)) {
                this.killRing.resetLastYank();
            }
            if (!"kill-line".equals(ref) && !"kill-whole-line".equals(ref) && !"backward-kill-word".equals(ref) && !"kill-word".equals(ref)) {
                this.killRing.resetLastKill();
            }
        }
        return o;
    }
    
    @Override
    public ParsedLine getParsedLine() {
        return this.parsedLine;
    }
    
    @Override
    public String getLastBinding() {
        return this.bindingReader.getLastBinding();
    }
    
    @Override
    public String getSearchTerm() {
        return (this.searchTerm != null) ? this.searchTerm.toString() : null;
    }
    
    @Override
    public RegionType getRegionActive() {
        return this.regionActive;
    }
    
    @Override
    public int getRegionMark() {
        return this.regionMark;
    }
    
    @Override
    public boolean setKeyMap(final String name) {
        final KeyMap<Binding> map = this.keyMaps.get(name);
        if (map == null) {
            return false;
        }
        this.keyMap = name;
        if (this.reading) {
            this.callWidget("callback-keymap");
        }
        return true;
    }
    
    @Override
    public String getKeyMap() {
        return this.keyMap;
    }
    
    @Override
    public LineReader variable(final String name, final Object value) {
        this.variables.put(name, value);
        return this;
    }
    
    @Override
    public Map<String, Object> getVariables() {
        return this.variables;
    }
    
    @Override
    public Object getVariable(final String name) {
        return this.variables.get(name);
    }
    
    @Override
    public void setVariable(final String name, final Object value) {
        this.variables.put(name, value);
    }
    
    @Override
    public LineReader option(final Option option, final boolean value) {
        this.options.put(option, value);
        return this;
    }
    
    @Override
    public boolean isSet(final Option option) {
        return option.isSet(this.options);
    }
    
    @Override
    public void setOpt(final Option option) {
        this.options.put(option, Boolean.TRUE);
    }
    
    @Override
    public void unsetOpt(final Option option) {
        this.options.put(option, Boolean.FALSE);
    }
    
    @Override
    public void addCommandsInBuffer(final Collection<String> commands) {
        this.commandsBuffer.addAll(commands);
    }
    
    @Override
    public void editAndAddInBuffer(final Path file) throws Exception {
        if (this.isSet(Option.BRACKETED_PASTE)) {
            this.terminal.writer().write("\u001b[?2004l");
        }
        final Constructor<?> ctor = Class.forName("org.jline.builtins.Nano").getConstructor(Terminal.class, Path.class);
        final Editor editor = (Editor)ctor.newInstance(this.terminal, file.getParent());
        editor.setRestricted(true);
        editor.open(Collections.singletonList(file.getFileName().toString()));
        editor.run();
        try (final BufferedReader br = Files.newBufferedReader(file)) {
            this.commandsBuffer.clear();
            String line;
            while ((line = br.readLine()) != null) {
                this.commandsBuffer.add(line);
            }
        }
    }
    
    protected int getTabWidth() {
        return this.getInt("tab-width", 4);
    }
    
    protected String finishBuffer() {
        return this.finish(this.buf.toString());
    }
    
    protected String finish(String str) {
        String historyLine = str;
        if (!this.isSet(Option.DISABLE_EVENT_EXPANSION)) {
            final StringBuilder sb = new StringBuilder();
            boolean escaped = false;
            for (int i = 0; i < str.length(); ++i) {
                final char ch = str.charAt(i);
                if (escaped) {
                    escaped = false;
                    if (ch != '\n') {
                        sb.append(ch);
                    }
                }
                else if (this.parser.isEscapeChar(ch)) {
                    escaped = true;
                }
                else {
                    sb.append(ch);
                }
            }
            str = sb.toString();
        }
        if (this.maskingCallback != null) {
            historyLine = this.maskingCallback.history(historyLine);
        }
        if (historyLine != null && historyLine.length() > 0) {
            this.history.add(Instant.now(), historyLine);
        }
        return str;
    }
    
    protected synchronized void handleSignal(final Terminal.Signal signal) {
        this.doAutosuggestion = false;
        if (signal == Terminal.Signal.WINCH) {
            this.size.copy(this.terminal.getBufferSize());
            this.display.resize(this.size.getRows(), this.size.getColumns());
            final Status status = Status.getStatus(this.terminal, false);
            if (status != null) {
                status.resize(this.size);
                status.reset();
            }
            this.terminal.puts(InfoCmp.Capability.carriage_return, new Object[0]);
            this.terminal.puts(InfoCmp.Capability.clr_eos, new Object[0]);
            this.redrawLine();
            this.redisplay();
        }
        else if (signal == Terminal.Signal.CONT) {
            this.terminal.enterRawMode();
            this.size.copy(this.terminal.getBufferSize());
            this.display.resize(this.size.getRows(), this.size.getColumns());
            this.terminal.puts(InfoCmp.Capability.keypad_xmit, new Object[0]);
            this.redrawLine();
            this.redisplay();
        }
    }
    
    protected Widget getWidget(final Object binding) {
        Widget w;
        if (binding instanceof Widget) {
            w = (Widget)binding;
        }
        else if (binding instanceof Macro) {
            final String macro = ((Macro)binding).getSequence();
            w = (() -> {
                this.bindingReader.runMacro(macro);
                return true;
            });
        }
        else if (binding instanceof Reference) {
            final String name = ((Reference)binding).name();
            w = this.widgets.get(name);
            if (w == null) {
                w = (() -> {
                    this.post = (Supplier<AttributedString>)(() -> {
                        new AttributedString("No such widget `" + name + "'");
                        final AttributedString attributedString;
                        return (Boolean)attributedString;
                    });
                    return false;
                });
            }
        }
        else {
            w = (() -> {
                this.post = (Supplier<AttributedString>)(() -> new AttributedString("Unsupported widget"));
                return false;
            });
        }
        return w;
    }
    
    public void setPrompt(final String prompt) {
        this.prompt = ((prompt == null) ? AttributedString.EMPTY : this.expandPromptPattern(prompt, 0, "", 0));
    }
    
    public void setRightPrompt(final String rightPrompt) {
        this.rightPrompt = ((rightPrompt == null) ? AttributedString.EMPTY : this.expandPromptPattern(rightPrompt, 0, "", 0));
    }
    
    protected void setBuffer(final Buffer buffer) {
        this.buf.copyFrom(buffer);
    }
    
    protected void setBuffer(final String buffer) {
        this.buf.clear();
        this.buf.write(buffer);
    }
    
    protected String viDeleteChangeYankToRemap(final String op) {
        switch (op) {
            case "abort":
            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-to":
            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(final int ch) {
        if (Character.isUpperCase(ch)) {
            return Character.toLowerCase(ch);
        }
        if (Character.isLowerCase(ch)) {
            return Character.toUpperCase(ch);
        }
        return ch;
    }
    
    protected boolean isInViMoveOperation() {
        return this.viMoveMode != ViMoveMode.NORMAL;
    }
    
    protected boolean isInViChangeOperation() {
        return this.viMoveMode == ViMoveMode.CHANGE;
    }
    
    protected boolean isInViCmdMode() {
        return "vicmd".equals(this.keyMap);
    }
    
    protected boolean viForwardChar() {
        if (this.count < 0) {
            return this.callNeg(this::viBackwardChar);
        }
        int lim = this.findeol();
        if (this.isInViCmdMode() && !this.isInViMoveOperation()) {
            --lim;
        }
        if (this.buf.cursor() >= lim) {
            return false;
        }
        while (this.count-- > 0 && this.buf.cursor() < lim) {
            this.buf.move(1);
        }
        return true;
    }
    
    protected boolean viBackwardChar() {
        if (this.count < 0) {
            return this.callNeg(this::viForwardChar);
        }
        final int lim = this.findbol();
        if (this.buf.cursor() == lim) {
            return false;
        }
        while (this.count-- > 0 && this.buf.cursor() > 0) {
            this.buf.move(-1);
            if (this.buf.currChar() == 10) {
                this.buf.move(1);
                break;
            }
        }
        return true;
    }
    
    protected boolean forwardWord() {
        if (this.count < 0) {
            return this.callNeg(this::backwardWord);
        }
        while (this.count-- > 0) {
            while (this.buf.cursor() < this.buf.length() && this.isWord(this.buf.currChar())) {
                this.buf.move(1);
            }
            if (this.isInViChangeOperation() && this.count == 0) {
                break;
            }
            while (this.buf.cursor() < this.buf.length() && !this.isWord(this.buf.currChar())) {
                this.buf.move(1);
            }
        }
        return true;
    }
    
    protected boolean viForwardWord() {
        if (this.count < 0) {
            return this.callNeg(this::viBackwardWord);
        }
        while (this.count-- > 0) {
            if (this.isViAlphaNum(this.buf.currChar())) {
                while (this.buf.cursor() < this.buf.length() && this.isViAlphaNum(this.buf.currChar())) {
                    this.buf.move(1);
                }
            }
            else {
                while (this.buf.cursor() < this.buf.length() && !this.isViAlphaNum(this.buf.currChar()) && !this.isWhitespace(this.buf.currChar())) {
                    this.buf.move(1);
                }
            }
            if (this.isInViChangeOperation() && this.count == 0) {
                return true;
            }
            for (int nl = (this.buf.currChar() == 10) ? 1 : 0; this.buf.cursor() < this.buf.length() && nl < 2 && this.isWhitespace(this.buf.currChar()); nl += ((this.buf.currChar() == 10) ? 1 : 0)) {
                this.buf.move(1);
            }
        }
        return true;
    }
    
    protected boolean viForwardBlankWord() {
        if (this.count < 0) {
            return this.callNeg(this::viBackwardBlankWord);
        }
        while (this.count-- > 0) {
            while (this.buf.cursor() < this.buf.length() && !this.isWhitespace(this.buf.currChar())) {
                this.buf.move(1);
            }
            if (this.isInViChangeOperation() && this.count == 0) {
                return true;
            }
            for (int nl = (this.buf.currChar() == 10) ? 1 : 0; this.buf.cursor() < this.buf.length() && nl < 2 && this.isWhitespace(this.buf.currChar()); nl += ((this.buf.currChar() == 10) ? 1 : 0)) {
                this.buf.move(1);
            }
        }
        return true;
    }
    
    protected boolean emacsForwardWord() {
        return this.forwardWord();
    }
    
    protected boolean viForwardBlankWordEnd() {
        if (this.count < 0) {
            return false;
        }
        while (this.count-- > 0) {
            while (this.buf.cursor() < this.buf.length()) {
                this.buf.move(1);
                if (!this.isWhitespace(this.buf.currChar())) {
                    break;
                }
            }
            while (this.buf.cursor() < this.buf.length()) {
                this.buf.move(1);
                if (this.isWhitespace(this.buf.currChar())) {
                    break;
                }
            }
        }
        return true;
    }
    
    protected boolean viForwardWordEnd() {
        if (this.count < 0) {
            return this.callNeg(this::backwardWord);
        }
        while (this.count-- > 0) {
            while (this.buf.cursor() < this.buf.length() && this.isWhitespace(this.buf.nextChar())) {
                this.buf.move(1);
            }
            if (this.buf.cursor() < this.buf.length()) {
                if (this.isViAlphaNum(this.buf.nextChar())) {
                    this.buf.move(1);
                    while (this.buf.cursor() < this.buf.length() && this.isViAlphaNum(this.buf.nextChar())) {
                        this.buf.move(1);
                    }
                }
                else {
                    this.buf.move(1);
                    while (this.buf.cursor() < this.buf.length() && !this.isViAlphaNum(this.buf.nextChar()) && !this.isWhitespace(this.buf.nextChar())) {
                        this.buf.move(1);
                    }
                }
            }
        }
        if (this.buf.cursor() < this.buf.length() && this.isInViMoveOperation()) {
            this.buf.move(1);
        }
        return true;
    }
    
    protected boolean backwardWord() {
        if (this.count < 0) {
            return this.callNeg(this::forwardWord);
        }
        while (this.count-- > 0) {
            while (this.buf.cursor() > 0 && !this.isWord(this.buf.atChar(this.buf.cursor() - 1))) {
                this.buf.move(-1);
            }
            while (this.buf.cursor() > 0 && this.isWord(this.buf.atChar(this.buf.cursor() - 1))) {
                this.buf.move(-1);
            }
        }
        return true;
    }
    
    protected boolean viBackwardWord() {
        if (this.count < 0) {
            return this.callNeg(this::viForwardWord);
        }
        while (this.count-- > 0) {
            int nl = 0;
            while (this.buf.cursor() > 0) {
                this.buf.move(-1);
                if (!this.isWhitespace(this.buf.currChar())) {
                    break;
                }
                nl += ((this.buf.currChar() == 10) ? 1 : 0);
                if (nl == 2) {
                    this.buf.move(1);
                    break;
                }
            }
            if (this.buf.cursor() > 0) {
                if (this.isViAlphaNum(this.buf.currChar())) {
                    while (this.buf.cursor() > 0) {
                        if (!this.isViAlphaNum(this.buf.prevChar())) {
                            break;
                        }
                        this.buf.move(-1);
                    }
                }
                else {
                    while (this.buf.cursor() > 0 && !this.isViAlphaNum(this.buf.prevChar())) {
                        if (this.isWhitespace(this.buf.prevChar())) {
                            break;
                        }
                        this.buf.move(-1);
                    }
                }
            }
        }
        return true;
    }
    
    protected boolean viBackwardBlankWord() {
        if (this.count < 0) {
            return this.callNeg(this::viForwardBlankWord);
        }
        while (this.count-- > 0) {
            while (this.buf.cursor() > 0) {
                this.buf.move(-1);
                if (!this.isWhitespace(this.buf.currChar())) {
                    break;
                }
            }
            while (this.buf.cursor() > 0) {
                this.buf.move(-1);
                if (this.isWhitespace(this.buf.currChar())) {
                    break;
                }
            }
        }
        return true;
    }
    
    protected boolean viBackwardWordEnd() {
        if (this.count < 0) {
            return this.callNeg(this::viForwardWordEnd);
        }
        while (this.count-- > 0 && this.buf.cursor() > 1) {
            int start;
            if (this.isViAlphaNum(this.buf.currChar())) {
                start = 1;
            }
            else if (!this.isWhitespace(this.buf.currChar())) {
                start = 2;
            }
            else {
                start = 0;
            }
            while (this.buf.cursor() > 0) {
                boolean same = start != 1 && this.isWhitespace(this.buf.currChar());
                if (start != 0) {
                    same |= this.isViAlphaNum(this.buf.currChar());
                }
                if (same == (start == 2)) {
                    break;
                }
                this.buf.move(-1);
            }
            while (this.buf.cursor() > 0 && this.isWhitespace(this.buf.currChar())) {
                this.buf.move(-1);
            }
        }
        return true;
    }
    
    protected boolean viBackwardBlankWordEnd() {
        if (this.count < 0) {
            return this.callNeg(this::viForwardBlankWordEnd);
        }
        while (this.count-- > 0) {
            while (this.buf.cursor() > 0 && !this.isWhitespace(this.buf.currChar())) {
                this.buf.move(-1);
            }
            while (this.buf.cursor() > 0 && this.isWhitespace(this.buf.currChar())) {
                this.buf.move(-1);
            }
        }
        return true;
    }
    
    protected boolean emacsBackwardWord() {
        return this.backwardWord();
    }
    
    protected boolean backwardDeleteWord() {
        if (this.count < 0) {
            return this.callNeg(this::deleteWord);
        }
        int cursor = this.buf.cursor();
        while (this.count-- > 0) {
            while (cursor > 0 && !this.isWord(this.buf.atChar(cursor - 1))) {
                --cursor;
            }
            while (cursor > 0 && this.isWord(this.buf.atChar(cursor - 1))) {
                --cursor;
            }
        }
        this.buf.backspace(this.buf.cursor() - cursor);
        return true;
    }
    
    protected boolean viBackwardKillWord() {
        if (this.count < 0) {
            return false;
        }
        final int lim = this.findbol();
        int x = this.buf.cursor();
        while (this.count-- > 0) {
            while (x > lim && this.isWhitespace(this.buf.atChar(x - 1))) {
                --x;
            }
            if (x > lim) {
                if (this.isViAlphaNum(this.buf.atChar(x - 1))) {
                    while (x > lim && this.isViAlphaNum(this.buf.atChar(x - 1))) {
                        --x;
                    }
                }
                else {
                    while (x > lim && !this.isViAlphaNum(this.buf.atChar(x - 1)) && !this.isWhitespace(this.buf.atChar(x - 1))) {
                        --x;
                    }
                }
            }
        }
        this.killRing.addBackwards(this.buf.substring(x, this.buf.cursor()));
        this.buf.backspace(this.buf.cursor() - x);
        return true;
    }
    
    protected boolean backwardKillWord() {
        if (this.count < 0) {
            return this.callNeg(this::killWord);
        }
        int x = this.buf.cursor();
        while (this.count-- > 0) {
            while (x > 0 && !this.isWord(this.buf.atChar(x - 1))) {
                --x;
            }
            while (x > 0 && this.isWord(this.buf.atChar(x - 1))) {
                --x;
            }
        }
        this.killRing.addBackwards(this.buf.substring(x, this.buf.cursor()));
        this.buf.backspace(this.buf.cursor() - x);
        return true;
    }
    
    protected boolean copyPrevWord() {
        if (this.count <= 0) {
            return false;
        }
        int t0 = this.buf.cursor();
        do {
            final int t2 = t0;
            while (t0 > 0 && !this.isWord(this.buf.atChar(t0 - 1))) {
                --t0;
            }
            while (t0 > 0 && this.isWord(this.buf.atChar(t0 - 1))) {
                --t0;
            }
            if (--this.count == 0) {
                this.buf.write(this.buf.substring(t0, t2));
                return true;
            }
        } while (t0 != 0);
        return false;
    }
    
    protected boolean upCaseWord() {
        int count = Math.abs(this.count);
        final int cursor = this.buf.cursor();
        while (count-- > 0) {
            while (this.buf.cursor() < this.buf.length() && !this.isWord(this.buf.currChar())) {
                this.buf.move(1);
            }
            while (this.buf.cursor() < this.buf.length() && this.isWord(this.buf.currChar())) {
                this.buf.currChar(Character.toUpperCase(this.buf.currChar()));
                this.buf.move(1);
            }
        }
        if (this.count < 0) {
            this.buf.cursor(cursor);
        }
        return true;
    }
    
    protected boolean downCaseWord() {
        int count = Math.abs(this.count);
        final int cursor = this.buf.cursor();
        while (count-- > 0) {
            while (this.buf.cursor() < this.buf.length() && !this.isWord(this.buf.currChar())) {
                this.buf.move(1);
            }
            while (this.buf.cursor() < this.buf.length() && this.isWord(this.buf.currChar())) {
                this.buf.currChar(Character.toLowerCase(this.buf.currChar()));
                this.buf.move(1);
            }
        }
        if (this.count < 0) {
            this.buf.cursor(cursor);
        }
        return true;
    }
    
    protected boolean capitalizeWord() {
        int count = Math.abs(this.count);
        final int cursor = this.buf.cursor();
        while (count-- > 0) {
            boolean first = true;
            while (this.buf.cursor() < this.buf.length() && !this.isWord(this.buf.currChar())) {
                this.buf.move(1);
            }
            while (this.buf.cursor() < this.buf.length() && this.isWord(this.buf.currChar()) && !this.isAlpha(this.buf.currChar())) {
                this.buf.move(1);
            }
            while (this.buf.cursor() < this.buf.length() && this.isWord(this.buf.currChar())) {
                this.buf.currChar(first ? Character.toUpperCase(this.buf.currChar()) : Character.toLowerCase(this.buf.currChar()));
                this.buf.move(1);
                first = false;
            }
        }
        if (this.count < 0) {
            this.buf.cursor(cursor);
        }
        return true;
    }
    
    protected boolean deleteWord() {
        if (this.count < 0) {
            return this.callNeg(this::backwardDeleteWord);
        }
        int x = this.buf.cursor();
        while (this.count-- > 0) {
            while (x < this.buf.length() && !this.isWord(this.buf.atChar(x))) {
                ++x;
            }
            while (x < this.buf.length() && this.isWord(this.buf.atChar(x))) {
                ++x;
            }
        }
        this.buf.delete(x - this.buf.cursor());
        return true;
    }
    
    protected boolean killWord() {
        if (this.count < 0) {
            return this.callNeg(this::backwardKillWord);
        }
        int x = this.buf.cursor();
        while (this.count-- > 0) {
            while (x < this.buf.length() && !this.isWord(this.buf.atChar(x))) {
                ++x;
            }
            while (x < this.buf.length() && this.isWord(this.buf.atChar(x))) {
                ++x;
            }
        }
        this.killRing.add(this.buf.substring(this.buf.cursor(), x));
        this.buf.delete(x - this.buf.cursor());
        return true;
    }
    
    protected boolean transposeWords() {
        int lstart = this.buf.cursor() - 1;
        int lend = this.buf.cursor();
        while (this.buf.atChar(lstart) != 0 && this.buf.atChar(lstart) != 10) {
            --lstart;
        }
        ++lstart;
        while (this.buf.atChar(lend) != 0 && this.buf.atChar(lend) != 10) {
            ++lend;
        }
        if (lend - lstart < 2) {
            return false;
        }
        int words = 0;
        boolean inWord = false;
        if (!this.isDelimiter(this.buf.atChar(lstart))) {
            ++words;
            inWord = true;
        }
        for (int i = lstart; i < lend; ++i) {
            if (this.isDelimiter(this.buf.atChar(i))) {
                inWord = false;
            }
            else {
                if (!inWord) {
                    ++words;
                }
                inWord = true;
            }
        }
        if (words < 2) {
            return false;
        }
        final boolean neg = this.count < 0;
        for (int count = Math.max(this.count, -this.count); count > 0; --count) {
            int sta1;
            for (sta1 = this.buf.cursor(); sta1 > lstart && !this.isDelimiter(this.buf.atChar(sta1 - 1)); --sta1) {}
            int end1 = sta1;
            while (end1 < lend && !this.isDelimiter(this.buf.atChar(++end1))) {}
            int end2;
            int sta2;
            if (neg) {
                for (end2 = sta1 - 1; end2 > lstart && this.isDelimiter(this.buf.atChar(end2 - 1)); --end2) {}
                if (end2 < lstart) {
                    sta2 = end1;
                    while (this.isDelimiter(this.buf.atChar(++sta2))) {}
                    end2 = sta2;
                    while (end2 < lend && !this.isDelimiter(this.buf.atChar(++end2))) {}
                }
                else {
                    for (sta2 = end2; sta2 > lstart && !this.isDelimiter(this.buf.atChar(sta2 - 1)); --sta2) {}
                }
            }
            else {
                sta2 = end1;
                while (sta2 < lend && this.isDelimiter(this.buf.atChar(++sta2))) {}
                if (sta2 == lend) {
                    for (end2 = sta1; this.isDelimiter(this.buf.atChar(end2 - 1)); --end2) {}
                    for (sta2 = end2; sta2 > lstart && !this.isDelimiter(this.buf.atChar(sta2 - 1)); --sta2) {}
                }
                else {
                    end2 = sta2;
                    while (end2 < lend && !this.isDelimiter(this.buf.atChar(++end2))) {}
                }
            }
            if (sta1 < sta2) {
                final String res = this.buf.substring(0, sta1) + this.buf.substring(sta2, end2) + this.buf.substring(end1, sta2) + this.buf.substring(sta1, end1) + this.buf.substring(end2);
                this.buf.clear();
                this.buf.write(res);
                this.buf.cursor(neg ? end1 : end2);
            }
            else {
                final String res = this.buf.substring(0, sta2) + this.buf.substring(sta1, end1) + this.buf.substring(end2, sta1) + this.buf.substring(sta2, end2) + this.buf.substring(end1);
                this.buf.clear();
                this.buf.write(res);
                this.buf.cursor(neg ? end2 : end1);
            }
        }
        return true;
    }
    
    private int findbol() {
        int x;
        for (x = this.buf.cursor(); x > 0 && this.buf.atChar(x - 1) != 10; --x) {}
        return x;
    }
    
    private int findeol() {
        int x;
        for (x = this.buf.cursor(); x < this.buf.length() && this.buf.atChar(x) != 10; ++x) {}
        return x;
    }
    
    protected boolean insertComment() {
        return this.doInsertComment(false);
    }
    
    protected boolean viInsertComment() {
        return this.doInsertComment(true);
    }
    
    protected boolean doInsertComment(final boolean isViMode) {
        final String comment = this.getString("comment-begin", "#");
        this.beginningOfLine();
        this.putString(comment);
        if (isViMode) {
            this.setKeyMap("viins");
        }
        return this.acceptLine();
    }
    
    protected boolean viFindNextChar() {
        final int vigetkey = this.vigetkey();
        this.findChar = vigetkey;
        if (vigetkey > 0) {
            this.findDir = 1;
            this.findTailAdd = 0;
            return this.vifindchar(false);
        }
        return false;
    }
    
    protected boolean viFindPrevChar() {
        final int vigetkey = this.vigetkey();
        this.findChar = vigetkey;
        if (vigetkey > 0) {
            this.findDir = -1;
            this.findTailAdd = 0;
            return this.vifindchar(false);
        }
        return false;
    }
    
    protected boolean viFindNextCharSkip() {
        final int vigetkey = this.vigetkey();
        this.findChar = vigetkey;
        if (vigetkey > 0) {
            this.findDir = 1;
            this.findTailAdd = -1;
            return this.vifindchar(false);
        }
        return false;
    }
    
    protected boolean viFindPrevCharSkip() {
        final int vigetkey = this.vigetkey();
        this.findChar = vigetkey;
        if (vigetkey > 0) {
            this.findDir = -1;
            this.findTailAdd = 1;
            return this.vifindchar(false);
        }
        return false;
    }
    
    protected boolean viRepeatFind() {
        return this.vifindchar(true);
    }
    
    protected boolean viRevRepeatFind() {
        if (this.count < 0) {
            return this.callNeg(() -> this.vifindchar(true));
        }
        this.findTailAdd = -this.findTailAdd;
        this.findDir = -this.findDir;
        final boolean ret = this.vifindchar(true);
        this.findTailAdd = -this.findTailAdd;
        this.findDir = -this.findDir;
        return ret;
    }
    
    private int vigetkey() {
        final int ch = this.readCharacter();
        final KeyMap<Binding> km = this.keyMaps.get("main");
        if (km != null) {
            final Binding b = km.getBound(new String(Character.toChars(ch)));
            if (b instanceof Reference) {
                final String func = ((Reference)b).name();
                if ("abort".equals(func)) {
                    return -1;
                }
            }
        }
        return ch;
    }
    
    private boolean vifindchar(final boolean repeat) {
        if (this.findDir == 0) {
            return false;
        }
        if (this.count < 0) {
            return this.callNeg(this::viRevRepeatFind);
        }
        if (repeat && this.findTailAdd != 0) {
            if (this.findDir > 0) {
                if (this.buf.cursor() < this.buf.length() && this.buf.nextChar() == this.findChar) {
                    this.buf.move(1);
                }
            }
            else if (this.buf.cursor() > 0 && this.buf.prevChar() == this.findChar) {
                this.buf.move(-1);
            }
        }
        final int cursor = this.buf.cursor();
        while (this.count-- > 0) {
            do {
                this.buf.move(this.findDir);
            } while (this.buf.cursor() > 0 && this.buf.cursor() < this.buf.length() && this.buf.currChar() != this.findChar && this.buf.currChar() != 10);
            if (this.buf.cursor() <= 0 || this.buf.cursor() >= this.buf.length() || this.buf.currChar() == 10) {
                this.buf.cursor(cursor);
                return false;
            }
        }
        if (this.findTailAdd != 0) {
            this.buf.move(this.findTailAdd);
        }
        if (this.findDir == 1 && this.isInViMoveOperation()) {
            this.buf.move(1);
        }
        return true;
    }
    
    private boolean callNeg(final Widget widget) {
        this.count = -this.count;
        final boolean ret = widget.apply();
        this.count = -this.count;
        return ret;
    }
    
    protected boolean viHistorySearchForward() {
        this.searchDir = 1;
        this.searchIndex = 0;
        return this.getViSearchString() && this.viRepeatSearch();
    }
    
    protected boolean viHistorySearchBackward() {
        this.searchDir = -1;
        this.searchIndex = this.history.size() - 1;
        return this.getViSearchString() && this.viRepeatSearch();
    }
    
    protected boolean viRepeatSearch() {
        if (this.searchDir == 0) {
            return false;
        }
        final int si = (this.searchDir < 0) ? this.searchBackwards(this.searchString, this.searchIndex, false) : this.searchForwards(this.searchString, this.searchIndex, false);
        if (si == -1 || si == this.history.index()) {
            return false;
        }
        this.searchIndex = si;
        this.buf.clear();
        this.history.moveTo(this.searchIndex);
        this.buf.write(this.history.get(this.searchIndex));
        if ("vicmd".equals(this.keyMap)) {
            this.buf.move(-1);
        }
        return true;
    }
    
    protected boolean viRevRepeatSearch() {
        this.searchDir = -this.searchDir;
        final boolean ret = this.viRepeatSearch();
        this.searchDir = -this.searchDir;
        return ret;
    }
    
    private boolean getViSearchString() {
        if (this.searchDir == 0) {
            return false;
        }
        final String searchPrompt = (this.searchDir < 0) ? "?" : "/";
        final Buffer searchBuffer = new BufferImpl();
        KeyMap<Binding> keyMap = this.keyMaps.get("main");
        if (keyMap == null) {
            keyMap = this.keyMaps.get(".safe");
        }
        while (true) {
            this.post = (Supplier<AttributedString>)(() -> {
                new AttributedString(searchPrompt + searchBuffer.toString() + "_");
                return;
            });
            this.redisplay();
            final Binding b = this.doReadBinding(keyMap, null);
            if (b instanceof Reference) {
                final String name;
                final String func = name = ((Reference)b).name();
                switch (name) {
                    case "abort": {
                        this.post = null;
                        return false;
                    }
                    case "accept-line":
                    case "vi-cmd-mode": {
                        this.searchString = searchBuffer.toString();
                        this.post = null;
                        return true;
                    }
                    case "magic-space": {
                        searchBuffer.write(32);
                        continue;
                    }
                    case "redisplay": {
                        this.redisplay();
                        continue;
                    }
                    case "clear-screen": {
                        this.clearScreen();
                        continue;
                    }
                    case "self-insert": {
                        searchBuffer.write(this.getLastBinding());
                        continue;
                    }
                    case "self-insert-unmeta": {
                        if (this.getLastBinding().charAt(0) == '\u001b') {
                            String s = this.getLastBinding().substring(1);
                            if ("\r".equals(s)) {
                                s = "\n";
                            }
                            searchBuffer.write(s);
                            continue;
                        }
                        continue;
                    }
                    case "backward-delete-char":
                    case "vi-backward-delete-char": {
                        if (searchBuffer.length() > 0) {
                            searchBuffer.backspace();
                            continue;
                        }
                        continue;
                    }
                    case "backward-kill-word":
                    case "vi-backward-kill-word": {
                        if (searchBuffer.length() > 0 && !this.isWhitespace(searchBuffer.prevChar())) {
                            searchBuffer.backspace();
                        }
                        if (searchBuffer.length() > 0 && this.isWhitespace(searchBuffer.prevChar())) {
                            searchBuffer.backspace();
                            continue;
                        }
                        continue;
                    }
                    case "quoted-insert":
                    case "vi-quoted-insert": {
                        final int c = this.readCharacter();
                        if (c >= 0) {
                            searchBuffer.write(c);
                            continue;
                        }
                        this.beep();
                        continue;
                    }
                    default: {
                        this.beep();
                        continue;
                    }
                }
            }
        }
    }
    
    protected boolean insertCloseCurly() {
        return this.insertClose("}");
    }
    
    protected boolean insertCloseParen() {
        return this.insertClose(")");
    }
    
    protected boolean insertCloseSquare() {
        return this.insertClose("]");
    }
    
    protected boolean insertClose(final String s) {
        this.putString(s);
        final long blink = this.getLong("blink-matching-paren", 500L);
        if (blink <= 0L) {
            this.removeIndentation();
            return true;
        }
        final int closePosition = this.buf.cursor();
        this.buf.move(-1);
        this.doViMatchBracket();
        this.redisplay();
        this.peekCharacter(blink);
        final int blinkPosition = this.buf.cursor();
        this.buf.cursor(closePosition);
        if (blinkPosition != closePosition - 1) {
            this.removeIndentation();
        }
        return true;
    }
    
    private void removeIndentation() {
        final int indent = this.getInt("indentation", 0);
        if (indent > 0) {
            this.buf.move(-1);
            for (int i = 0; i < indent; ++i) {
                this.buf.move(-1);
                if (this.buf.currChar() != 32) {
                    this.buf.move(1);
                    break;
                }
                this.buf.delete();
            }
            this.buf.move(1);
        }
    }
    
    protected boolean viMatchBracket() {
        return this.doViMatchBracket();
    }
    
    protected boolean undefinedKey() {
        return false;
    }
    
    protected boolean doViMatchBracket() {
        int pos = this.buf.cursor();
        if (pos == this.buf.length()) {
            return false;
        }
        final int type = this.getBracketType(this.buf.atChar(pos));
        final int move = (type < 0) ? -1 : 1;
        int count = 1;
        if (type == 0) {
            return false;
        }
        while (count > 0) {
            pos += move;
            if (pos < 0 || pos >= this.buf.length()) {
                return false;
            }
            final int curType = this.getBracketType(this.buf.atChar(pos));
            if (curType == type) {
                ++count;
            }
            else {
                if (curType != -type) {
                    continue;
                }
                --count;
            }
        }
        if (move > 0 && this.isInViMoveOperation()) {
            ++pos;
        }
        this.buf.cursor(pos);
        return true;
    }
    
    protected int getBracketType(final int ch) {
        switch (ch) {
            case 91: {
                return 1;
            }
            case 93: {
                return -1;
            }
            case 123: {
                return 2;
            }
            case 125: {
                return -2;
            }
            case 40: {
                return 3;
            }
            case 41: {
                return -3;
            }
            default: {
                return 0;
            }
        }
    }
    
    protected boolean transposeChars() {
        int lstart = this.buf.cursor() - 1;
        int lend = this.buf.cursor();
        while (this.buf.atChar(lstart) != 0 && this.buf.atChar(lstart) != 10) {
            --lstart;
        }
        ++lstart;
        while (this.buf.atChar(lend) != 0 && this.buf.atChar(lend) != 10) {
            ++lend;
        }
        if (lend - lstart < 2) {
            return false;
        }
        final boolean neg = this.count < 0;
        for (int count = Math.max(this.count, -this.count); count > 0; --count) {
            while (this.buf.cursor() <= lstart) {
                this.buf.move(1);
            }
            while (this.buf.cursor() >= lend) {
                this.buf.move(-1);
            }
            final int c = this.buf.currChar();
            this.buf.currChar(this.buf.prevChar());
            this.buf.move(-1);
            this.buf.currChar(c);
            this.buf.move(neg ? 0 : 2);
        }
        return true;
    }
    
    protected boolean undo() {
        this.isUndo = true;
        if (this.undo.canUndo()) {
            this.undo.undo();
            return true;
        }
        return false;
    }
    
    protected boolean redo() {
        this.isUndo = true;
        if (this.undo.canRedo()) {
            this.undo.redo();
            return true;
        }
        return false;
    }
    
    protected boolean sendBreak() {
        if (this.searchTerm == null) {
            this.buf.clear();
            this.println();
            this.redrawLine();
            return false;
        }
        return true;
    }
    
    protected boolean backwardChar() {
        return this.buf.move(-this.count) != 0;
    }
    
    protected boolean forwardChar() {
        return this.buf.move(this.count) != 0;
    }
    
    protected boolean viDigitOrBeginningOfLine() {
        if (this.repeatCount > 0) {
            return this.digitArgument();
        }
        return this.beginningOfLine();
    }
    
    protected boolean universalArgument() {
        this.mult *= this.universal;
        return this.isArgDigit = true;
    }
    
    protected boolean argumentBase() {
        if (this.repeatCount > 0 && this.repeatCount < 32) {
            this.universal = this.repeatCount;
            return this.isArgDigit = true;
        }
        return false;
    }
    
    protected boolean negArgument() {
        this.mult *= -1;
        return this.isArgDigit = true;
    }
    
    protected boolean digitArgument() {
        final String s = this.getLastBinding();
        this.repeatCount = this.repeatCount * 10 + s.charAt(s.length() - 1) - 48;
        final int maxRepeatCount = this.getInt("max-repeat-count", 9999);
        if (this.repeatCount > maxRepeatCount) {
            throw new IllegalArgumentException("digit argument should be less than " + maxRepeatCount);
        }
        return this.isArgDigit = true;
    }
    
    protected boolean viDelete() {
        final int cursorStart = this.buf.cursor();
        final Binding o = this.readBinding(this.getKeys());
        if (o instanceof Reference) {
            final String op = this.viDeleteChangeYankToRemap(((Reference)o).name());
            if ("vi-delete".equals(op)) {
                this.killWholeLine();
            }
            else {
                this.viMoveMode = ViMoveMode.DELETE;
                final Widget widget = this.widgets.get(op);
                if (widget != null && !widget.apply()) {
                    this.viMoveMode = ViMoveMode.NORMAL;
                    return false;
                }
                this.viMoveMode = ViMoveMode.NORMAL;
            }
            return this.viDeleteTo(cursorStart, this.buf.cursor());
        }
        this.pushBackBinding();
        return false;
    }
    
    protected boolean viYankTo() {
        final int cursorStart = this.buf.cursor();
        final Binding o = this.readBinding(this.getKeys());
        if (!(o instanceof Reference)) {
            this.pushBackBinding();
            return false;
        }
        final String op = this.viDeleteChangeYankToRemap(((Reference)o).name());
        if ("vi-yank".equals(op)) {
            this.yankBuffer = this.buf.toString();
            return true;
        }
        this.viMoveMode = ViMoveMode.YANK;
        final Widget widget = this.widgets.get(op);
        if (widget != null && !widget.apply()) {
            return false;
        }
        this.viMoveMode = ViMoveMode.NORMAL;
        return this.viYankTo(cursorStart, this.buf.cursor());
    }
    
    protected boolean viYankWholeLine() {
        final int p = this.buf.cursor();
        while (this.buf.move(-1) == -1 && this.buf.prevChar() != 10) {}
        final int s = this.buf.cursor();
        for (int i = 0; i < this.repeatCount; ++i) {
            while (this.buf.move(1) == 1 && this.buf.prevChar() != 10) {}
        }
        final int e = this.buf.cursor();
        this.yankBuffer = this.buf.substring(s, e);
        if (!this.yankBuffer.endsWith("\n")) {
            this.yankBuffer += "\n";
        }
        this.buf.cursor(p);
        return true;
    }
    
    protected boolean viChange() {
        final int cursorStart = this.buf.cursor();
        final Binding o = this.readBinding(this.getKeys());
        if (o instanceof Reference) {
            final String op = this.viDeleteChangeYankToRemap(((Reference)o).name());
            if ("vi-change-to".equals(op)) {
                this.killWholeLine();
            }
            else {
                this.viMoveMode = ViMoveMode.CHANGE;
                final Widget widget = this.widgets.get(op);
                if (widget != null && !widget.apply()) {
                    this.viMoveMode = ViMoveMode.NORMAL;
                    return false;
                }
                this.viMoveMode = ViMoveMode.NORMAL;
            }
            final boolean res = this.viChange(cursorStart, this.buf.cursor());
            this.setKeyMap("viins");
            return res;
        }
        this.pushBackBinding();
        return false;
    }
    
    protected void cleanup() {
        if (this.isSet(Option.ERASE_LINE_ON_FINISH)) {
            final Buffer oldBuffer = this.buf.copy();
            final AttributedString oldPrompt = this.prompt;
            this.buf.clear();
            this.prompt = new AttributedString("");
            this.doCleanup(false);
            this.prompt = oldPrompt;
            this.buf.copyFrom(oldBuffer);
        }
        else {
            this.doCleanup(true);
        }
    }
    
    protected void doCleanup(final boolean nl) {
        this.buf.cursor(this.buf.length());
        this.post = null;
        if (this.size.getColumns() > 0 || this.size.getRows() > 0) {
            this.redisplay(this.doAutosuggestion = false);
            if (nl) {
                this.println();
            }
            this.terminal.puts(InfoCmp.Capability.keypad_local, new Object[0]);
            this.terminal.trackMouse(Terminal.MouseTracking.Off);
            if (this.isSet(Option.BRACKETED_PASTE) && !this.isTerminalDumb()) {
                this.terminal.writer().write("\u001b[?2004l");
            }
            this.stopMaskThread();
            this.flush();
        }
        this.history.moveToEnd();
    }
    
    protected boolean historyIncrementalSearchForward() {
        return this.doSearchHistory(false);
    }
    
    protected boolean historyIncrementalSearchBackward() {
        return this.doSearchHistory(true);
    }
    
    protected boolean doSearchHistory(final boolean backward) {
        if (this.history.isEmpty()) {
            return false;
        }
        final KeyMap<Binding> terminators = new KeyMap<Binding>();
        this.getString("search-terminators", "\u001b\n").codePoints().forEach(c -> this.bind(terminators, "accept-line", new String(Character.toChars(c))));
        final Buffer originalBuffer = this.buf.copy();
        this.searchIndex = -1;
        this.searchTerm = new StringBuffer();
        this.searchBackward = backward;
        this.searchFailing = false;
        this.post = (Supplier<AttributedString>)(() -> {
            new AttributedString((this.searchFailing ? "failing " : "") + (this.searchBackward ? "bck-i-search" : "fwd-i-search") + ": " + (Object)this.searchTerm + "_");
            return;
        });
        this.redisplay();
        try {
            while (true) {
                final int prevSearchIndex = this.searchIndex;
                final Binding operation = this.readBinding(this.getKeys(), terminators);
                final String ref = (operation instanceof Reference) ? ((Reference)operation).name() : "";
                boolean next = false;
                final String s = ref;
                switch (s) {
                    case "abort": {
                        this.beep();
                        this.buf.copyFrom(originalBuffer);
                        return true;
                    }
                    case "history-incremental-search-backward": {
                        this.searchBackward = true;
                        next = true;
                        break;
                    }
                    case "history-incremental-search-forward": {
                        this.searchBackward = false;
                        next = true;
                        break;
                    }
                    case "backward-delete-char": {
                        if (this.searchTerm.length() > 0) {
                            this.searchTerm.deleteCharAt(this.searchTerm.length() - 1);
                            break;
                        }
                        break;
                    }
                    case "self-insert": {
                        this.searchTerm.append(this.getLastBinding());
                        break;
                    }
                    default: {
                        if (this.searchIndex != -1) {
                            this.history.moveTo(this.searchIndex);
                        }
                        this.pushBackBinding();
                        return true;
                    }
                }
                final String pattern = this.doGetSearchPattern();
                if (pattern.length() == 0) {
                    this.buf.copyFrom(originalBuffer);
                    this.searchFailing = false;
                }
                else {
                    final boolean caseInsensitive = this.isSet(Option.CASE_INSENSITIVE_SEARCH);
                    final Pattern pat = Pattern.compile(pattern, caseInsensitive ? 66 : 64);
                    Pair<Integer, Integer> pair = null;
                    if (this.searchBackward) {
                        final boolean nextOnly = next;
                        pair = this.matches(pat, this.buf.toString(), this.searchIndex).stream().filter(p -> nextOnly ? ((int)p.v < this.buf.cursor()) : ((int)p.v <= this.buf.cursor())).max(Comparator.comparing((Function<? super Pair<Integer, Integer>, ? extends Comparable>)Pair::getV)).orElse(null);
                        if (pair == null) {
                            final IOError e;
                            pair = StreamSupport.stream(Spliterators.spliteratorUnknownSize((Iterator<?>)this.history.reverseIterator((this.searchIndex < 0) ? this.history.last() : (this.searchIndex - 1)), 16), false).flatMap(e -> this.matches(pat, e.line(), e.index()).stream()).findFirst().orElse(null);
                        }
                    }
                    else {
                        final boolean nextOnly = next;
                        pair = this.matches(pat, this.buf.toString(), this.searchIndex).stream().filter(p -> nextOnly ? ((int)p.v > this.buf.cursor()) : ((int)p.v >= this.buf.cursor())).min(Comparator.comparing((Function<? super Pair<Integer, Integer>, ? extends Comparable>)Pair::getV)).orElse(null);
                        if (pair == null) {
                            final IOError e;
                            pair = StreamSupport.stream(Spliterators.spliteratorUnknownSize((Iterator<?>)this.history.iterator(((this.searchIndex < 0) ? this.history.last() : this.searchIndex) + 1), 16), false).flatMap(e -> this.matches(pat, e.line(), e.index()).stream()).findFirst().orElse(null);
                            if (pair == null && this.searchIndex >= 0) {
                                pair = this.matches(pat, originalBuffer.toString(), -1).stream().min(Comparator.comparing((Function<? super Pair<Integer, Integer>, ? extends Comparable>)Pair::getV)).orElse(null);
                            }
                        }
                    }
                    if (pair != null) {
                        this.searchIndex = pair.u;
                        this.buf.clear();
                        if (this.searchIndex >= 0) {
                            this.buf.write(this.history.get(this.searchIndex));
                        }
                        else {
                            this.buf.write(originalBuffer.toString());
                        }
                        this.buf.cursor(pair.v);
                        this.searchFailing = false;
                    }
                    else {
                        this.searchFailing = true;
                        this.beep();
                    }
                }
                this.redisplay();
            }
        }
        catch (final IOError e) {
            if (!(e.getCause() instanceof InterruptedException)) {
                throw e;
            }
            return true;
        }
        finally {
            this.searchTerm = null;
            this.searchIndex = -1;
            this.post = null;
        }
    }
    
    private List<Pair<Integer, Integer>> matches(final Pattern p, final String line, final int index) {
        final List<Pair<Integer, Integer>> starts = new ArrayList<Pair<Integer, Integer>>();
        final Matcher m = p.matcher(line);
        while (m.find()) {
            starts.add(new Pair<Integer, Integer>(index, m.start()));
        }
        return starts;
    }
    
    private String doGetSearchPattern() {
        final StringBuilder sb = new StringBuilder();
        boolean inQuote = false;
        for (int i = 0; i < this.searchTerm.length(); ++i) {
            final char c = this.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() {
        this.pushBackBinding(false);
    }
    
    private void pushBackBinding(final boolean skip) {
        final String s = this.getLastBinding();
        if (s != null) {
            this.bindingReader.runMacro(s);
            this.skipRedisplay = skip;
        }
    }
    
    protected boolean historySearchForward() {
        if (this.historyBuffer == null || this.buf.length() == 0 || !this.buf.toString().equals(this.history.current())) {
            this.historyBuffer = this.buf.copy();
            this.searchBuffer = this.getFirstWord();
        }
        final int index = this.history.index() + 1;
        if (index < this.history.last() + 1) {
            final int searchIndex = this.searchForwards(this.searchBuffer.toString(), index, true);
            if (searchIndex == -1) {
                this.history.moveToEnd();
                if (this.buf.toString().equals(this.historyBuffer.toString())) {
                    return false;
                }
                this.setBuffer(this.historyBuffer.toString());
                this.historyBuffer = null;
            }
            else {
                if (!this.history.moveTo(searchIndex)) {
                    this.history.moveToEnd();
                    this.setBuffer(this.historyBuffer.toString());
                    return false;
                }
                this.setBuffer(this.history.current());
            }
        }
        else {
            this.history.moveToEnd();
            if (this.buf.toString().equals(this.historyBuffer.toString())) {
                return false;
            }
            this.setBuffer(this.historyBuffer.toString());
            this.historyBuffer = null;
        }
        return true;
    }
    
    private CharSequence getFirstWord() {
        String s;
        int i;
        for (s = this.buf.toString(), i = 0; i < s.length() && !Character.isWhitespace(s.charAt(i)); ++i) {}
        return s.substring(0, i);
    }
    
    protected boolean historySearchBackward() {
        if (this.historyBuffer == null || this.buf.length() == 0 || !this.buf.toString().equals(this.history.current())) {
            this.historyBuffer = this.buf.copy();
            this.searchBuffer = this.getFirstWord();
        }
        final int searchIndex = this.searchBackwards(this.searchBuffer.toString(), this.history.index(), true);
        if (searchIndex == -1) {
            return false;
        }
        if (this.history.moveTo(searchIndex)) {
            this.setBuffer(this.history.current());
            return true;
        }
        return false;
    }
    
    public int searchBackwards(final String searchTerm, final int startIndex) {
        return this.searchBackwards(searchTerm, startIndex, false);
    }
    
    public int searchBackwards(final String searchTerm) {
        return this.searchBackwards(searchTerm, this.history.index(), false);
    }
    
    public int searchBackwards(String searchTerm, final int startIndex, final boolean startsWith) {
        final boolean caseInsensitive = this.isSet(Option.CASE_INSENSITIVE_SEARCH);
        if (caseInsensitive) {
            searchTerm = searchTerm.toLowerCase();
        }
        final ListIterator<History.Entry> it = this.history.iterator(startIndex);
        while (it.hasPrevious()) {
            final History.Entry e = it.previous();
            String line = e.line();
            if (caseInsensitive) {
                line = line.toLowerCase();
            }
            final int idx = line.indexOf(searchTerm);
            if ((startsWith && idx == 0) || (!startsWith && idx >= 0)) {
                return e.index();
            }
        }
        return -1;
    }
    
    public int searchForwards(String searchTerm, int startIndex, final boolean startsWith) {
        final boolean caseInsensitive = this.isSet(Option.CASE_INSENSITIVE_SEARCH);
        if (caseInsensitive) {
            searchTerm = searchTerm.toLowerCase();
        }
        if (startIndex > this.history.last()) {
            startIndex = this.history.last();
        }
        final ListIterator<History.Entry> it = this.history.iterator(startIndex);
        if (this.searchIndex != -1 && it.hasNext()) {
            it.next();
        }
        while (it.hasNext()) {
            final History.Entry e = it.next();
            String line = e.line();
            if (caseInsensitive) {
                line = line.toLowerCase();
            }
            final int idx = line.indexOf(searchTerm);
            if ((startsWith && idx == 0) || (!startsWith && idx >= 0)) {
                return e.index();
            }
        }
        return -1;
    }
    
    public int searchForwards(final String searchTerm, final int startIndex) {
        return this.searchForwards(searchTerm, startIndex, false);
    }
    
    public int searchForwards(final String searchTerm) {
        return this.searchForwards(searchTerm, this.history.index());
    }
    
    protected boolean quit() {
        this.getBuffer().clear();
        return this.acceptLine();
    }
    
    protected boolean acceptAndHold() {
        this.nextCommandFromHistory = false;
        this.acceptLine();
        if (!this.buf.toString().isEmpty()) {
            this.nextHistoryId = Integer.MAX_VALUE;
            this.nextCommandFromHistory = true;
        }
        return this.nextCommandFromHistory;
    }
    
    protected boolean acceptLineAndDownHistory() {
        this.nextCommandFromHistory = false;
        this.acceptLine();
        if (this.nextHistoryId < 0) {
            this.nextHistoryId = this.history.index();
        }
        if (this.history.size() > this.nextHistoryId + 1) {
            ++this.nextHistoryId;
            this.nextCommandFromHistory = true;
        }
        return this.nextCommandFromHistory;
    }
    
    protected boolean acceptAndInferNextHistory() {
        this.nextCommandFromHistory = false;
        this.acceptLine();
        if (!this.buf.toString().isEmpty()) {
            this.nextHistoryId = this.searchBackwards(this.buf.toString(), this.history.last());
            if (this.nextHistoryId >= 0 && this.history.size() > this.nextHistoryId + 1) {
                ++this.nextHistoryId;
                this.nextCommandFromHistory = true;
            }
        }
        return this.nextCommandFromHistory;
    }
    
    protected boolean acceptLine() {
        this.parsedLine = null;
        int curPos = 0;
        if (!this.isSet(Option.DISABLE_EVENT_EXPANSION)) {
            try {
                final String str = this.buf.toString();
                final String exp = this.expander.expandHistory(this.history, str);
                if (!exp.equals(str)) {
                    this.buf.clear();
                    this.buf.write(exp);
                    if (this.isSet(Option.HISTORY_VERIFY)) {
                        return true;
                    }
                }
            }
            catch (final IllegalArgumentException ex) {}
        }
        try {
            curPos = this.buf.cursor();
            this.parsedLine = this.parser.parse(this.buf.toString(), this.buf.cursor(), Parser.ParseContext.ACCEPT_LINE);
        }
        catch (final EOFError e) {
            final StringBuilder sb = new StringBuilder("\n");
            this.indention(e.getOpenBrackets(), sb);
            final int curMove = sb.length();
            if (this.isSet(Option.INSERT_BRACKET) && e.getOpenBrackets() > 1 && e.getNextClosingBracket() != null) {
                sb.append('\n');
                this.indention(e.getOpenBrackets() - 1, sb);
                sb.append(e.getNextClosingBracket());
            }
            this.buf.write(sb.toString());
            this.buf.cursor(curPos + curMove);
            return true;
        }
        catch (final SyntaxError syntaxError) {}
        this.callWidget("callback-finish");
        this.state = State.DONE;
        return true;
    }
    
    void indention(final int nb, final StringBuilder sb) {
        for (int indent = this.getInt("indentation", 0) * nb, i = 0; i < indent; ++i) {
            sb.append(' ');
        }
    }
    
    protected boolean selfInsert() {
        for (int count = this.count; count > 0; --count) {
            this.putString(this.getLastBinding());
        }
        return true;
    }
    
    protected boolean selfInsertUnmeta() {
        if (this.getLastBinding().charAt(0) == '\u001b') {
            String s = this.getLastBinding().substring(1);
            if ("\r".equals(s)) {
                s = "\n";
            }
            for (int count = this.count; count > 0; --count) {
                this.putString(s);
            }
            return true;
        }
        return false;
    }
    
    protected boolean overwriteMode() {
        this.overTyping = !this.overTyping;
        return true;
    }
    
    protected boolean beginningOfBufferOrHistory() {
        if (this.findbol() != 0) {
            this.buf.cursor(0);
            return true;
        }
        return this.beginningOfHistory();
    }
    
    protected boolean beginningOfHistory() {
        if (this.history.moveToFirst()) {
            this.setBuffer(this.history.current());
            return true;
        }
        return false;
    }
    
    protected boolean endOfBufferOrHistory() {
        if (this.findeol() != this.buf.length()) {
            this.buf.cursor(this.buf.length());
            return true;
        }
        return this.endOfHistory();
    }
    
    protected boolean endOfHistory() {
        if (this.history.moveToLast()) {
            this.setBuffer(this.history.current());
            return true;
        }
        return false;
    }
    
    protected boolean beginningOfLineHist() {
        if (this.count < 0) {
            return this.callNeg(this::endOfLineHist);
        }
        while (this.count-- > 0) {
            final int bol = this.findbol();
            if (bol != this.buf.cursor()) {
                this.buf.cursor(bol);
            }
            else {
                this.moveHistory(false);
                this.buf.cursor(0);
            }
        }
        return true;
    }
    
    protected boolean endOfLineHist() {
        if (this.count < 0) {
            return this.callNeg(this::beginningOfLineHist);
        }
        while (this.count-- > 0) {
            final int eol = this.findeol();
            if (eol != this.buf.cursor()) {
                this.buf.cursor(eol);
            }
            else {
                this.moveHistory(true);
            }
        }
        return true;
    }
    
    protected boolean upHistory() {
        while (this.count-- > 0) {
            if (!this.moveHistory(false)) {
                return !this.isSet(Option.HISTORY_BEEP);
            }
        }
        return true;
    }
    
    protected boolean downHistory() {
        while (this.count-- > 0) {
            if (!this.moveHistory(true)) {
                return !this.isSet(Option.HISTORY_BEEP);
            }
        }
        return true;
    }
    
    protected boolean viUpLineOrHistory() {
        return this.upLine() || (this.upHistory() && this.viFirstNonBlank());
    }
    
    protected boolean viDownLineOrHistory() {
        return this.downLine() || (this.downHistory() && this.viFirstNonBlank());
    }
    
    protected boolean upLine() {
        return this.buf.up();
    }
    
    protected boolean downLine() {
        return this.buf.down();
    }
    
    protected boolean upLineOrHistory() {
        return this.upLine() || this.upHistory();
    }
    
    protected boolean upLineOrSearch() {
        return this.upLine() || this.historySearchBackward();
    }
    
    protected boolean downLineOrHistory() {
        return this.downLine() || this.downHistory();
    }
    
    protected boolean downLineOrSearch() {
        return this.downLine() || this.historySearchForward();
    }
    
    protected boolean viCmdMode() {
        if (this.state == State.NORMAL) {
            this.buf.move(-1);
        }
        return this.setKeyMap("vicmd");
    }
    
    protected boolean viInsert() {
        return this.setKeyMap("viins");
    }
    
    protected boolean viAddNext() {
        this.buf.move(1);
        return this.setKeyMap("viins");
    }
    
    protected boolean viAddEol() {
        return this.endOfLine() && this.setKeyMap("viins");
    }
    
    protected boolean emacsEditingMode() {
        return this.setKeyMap("emacs");
    }
    
    protected boolean viChangeWholeLine() {
        return this.viFirstNonBlank() && this.viChangeEol();
    }
    
    protected boolean viChangeEol() {
        return this.viChange(this.buf.cursor(), this.buf.length()) && this.setKeyMap("viins");
    }
    
    protected boolean viKillEol() {
        final int eol = this.findeol();
        if (this.buf.cursor() == eol) {
            return false;
        }
        this.killRing.add(this.buf.substring(this.buf.cursor(), eol));
        this.buf.delete(eol - this.buf.cursor());
        return true;
    }
    
    protected boolean quotedInsert() {
        final int c = this.readCharacter();
        while (this.count-- > 0) {
            this.putString(new String(Character.toChars(c)));
        }
        return true;
    }
    
    protected boolean viJoin() {
        if (this.buf.down()) {
            while (this.buf.move(-1) == -1 && this.buf.prevChar() != 10) {}
            this.buf.backspace();
            this.buf.write(32);
            this.buf.move(-1);
            return true;
        }
        return false;
    }
    
    protected boolean viKillWholeLine() {
        return this.killWholeLine() && this.setKeyMap("viins");
    }
    
    protected boolean viInsertBol() {
        return this.beginningOfLine() && this.setKeyMap("viins");
    }
    
    protected boolean backwardDeleteChar() {
        if (this.count < 0) {
            return this.callNeg(this::deleteChar);
        }
        if (this.buf.cursor() == 0) {
            return false;
        }
        this.buf.backspace(this.count);
        return true;
    }
    
    protected boolean viFirstNonBlank() {
        this.beginningOfLine();
        while (this.buf.cursor() < this.buf.length() && this.isWhitespace(this.buf.currChar())) {
            this.buf.move(1);
        }
        return true;
    }
    
    protected boolean viBeginningOfLine() {
        this.buf.cursor(this.findbol());
        return true;
    }
    
    protected boolean viEndOfLine() {
        if (this.count < 0) {
            return false;
        }
        while (this.count-- > 0) {
            this.buf.cursor(this.findeol() + 1);
        }
        this.buf.move(-1);
        return true;
    }
    
    protected boolean beginningOfLine() {
        while (this.count-- > 0) {
            while (this.buf.move(-1) == -1 && this.buf.prevChar() != 10) {}
        }
        return true;
    }
    
    protected boolean endOfLine() {
        while (this.count-- > 0) {
            while (this.buf.move(1) == 1 && this.buf.currChar() != 10) {}
        }
        return true;
    }
    
    protected boolean deleteChar() {
        if (this.count < 0) {
            return this.callNeg(this::backwardDeleteChar);
        }
        if (this.buf.cursor() == this.buf.length()) {
            return false;
        }
        this.buf.delete(this.count);
        return true;
    }
    
    protected boolean viBackwardDeleteChar() {
        for (int i = 0; i < this.count; ++i) {
            if (!this.buf.backspace()) {
                return false;
            }
        }
        return true;
    }
    
    protected boolean viDeleteChar() {
        for (int i = 0; i < this.count; ++i) {
            if (!this.buf.delete()) {
                return false;
            }
        }
        return true;
    }
    
    protected boolean viSwapCase() {
        for (int i = 0; i < this.count; ++i) {
            if (this.buf.cursor() >= this.buf.length()) {
                return false;
            }
            int ch = this.buf.atChar(this.buf.cursor());
            ch = this.switchCase(ch);
            this.buf.currChar(ch);
            this.buf.move(1);
        }
        return true;
    }
    
    protected boolean viReplaceChars() {
        final int c = this.readCharacter();
        if (c < 0 || c == 27 || c == 3) {
            return true;
        }
        for (int i = 0; i < this.count; ++i) {
            if (!this.buf.currChar((char)c)) {
                return false;
            }
            if (i < this.count - 1) {
                this.buf.move(1);
            }
        }
        return true;
    }
    
    protected boolean viChange(final int startPos, final int endPos) {
        return this.doViDeleteOrChange(startPos, endPos, true);
    }
    
    protected boolean viDeleteTo(final int startPos, final int endPos) {
        return this.doViDeleteOrChange(startPos, endPos, false);
    }
    
    protected boolean doViDeleteOrChange(int startPos, int endPos, final boolean isChange) {
        if (startPos == endPos) {
            return true;
        }
        if (endPos < startPos) {
            final int tmp = endPos;
            endPos = startPos;
            startPos = tmp;
        }
        this.buf.cursor(startPos);
        this.buf.delete(endPos - startPos);
        if (!isChange && startPos > 0 && startPos == this.buf.length()) {
            this.buf.move(-1);
        }
        return true;
    }
    
    protected boolean viYankTo(int startPos, int endPos) {
        final int cursorPos = startPos;
        if (endPos < startPos) {
            final int tmp = endPos;
            endPos = startPos;
            startPos = tmp;
        }
        if (startPos == endPos) {
            this.yankBuffer = "";
            return true;
        }
        this.yankBuffer = this.buf.substring(startPos, endPos);
        this.buf.cursor(cursorPos);
        return true;
    }
    
    protected boolean viOpenLineAbove() {
        while (this.buf.move(-1) == -1 && this.buf.prevChar() != 10) {}
        this.buf.write(10);
        this.buf.move(-1);
        return this.setKeyMap("viins");
    }
    
    protected boolean viOpenLineBelow() {
        while (this.buf.move(1) == 1 && this.buf.currChar() != 10) {}
        this.buf.write(10);
        return this.setKeyMap("viins");
    }
    
    protected boolean viPutAfter() {
        if (this.yankBuffer.indexOf(10) >= 0) {
            while (this.buf.move(1) == 1 && this.buf.currChar() != 10) {}
            this.buf.move(1);
            this.putString(this.yankBuffer);
            this.buf.move(-this.yankBuffer.length());
        }
        else if (!this.yankBuffer.isEmpty()) {
            if (this.buf.cursor() < this.buf.length()) {
                this.buf.move(1);
            }
            for (int i = 0; i < this.count; ++i) {
                this.putString(this.yankBuffer);
            }
            this.buf.move(-1);
        }
        return true;
    }
    
    protected boolean viPutBefore() {
        if (this.yankBuffer.indexOf(10) >= 0) {
            while (this.buf.move(-1) == -1 && this.buf.prevChar() != 10) {}
            this.putString(this.yankBuffer);
            this.buf.move(-this.yankBuffer.length());
        }
        else if (!this.yankBuffer.isEmpty()) {
            if (this.buf.cursor() > 0) {
                this.buf.move(-1);
            }
            for (int i = 0; i < this.count; ++i) {
                this.putString(this.yankBuffer);
            }
            this.buf.move(-1);
        }
        return true;
    }
    
    protected boolean doLowercaseVersion() {
        this.bindingReader.runMacro(this.getLastBinding().toLowerCase());
        return true;
    }
    
    protected boolean setMarkCommand() {
        if (this.count < 0) {
            this.regionActive = RegionType.NONE;
            return true;
        }
        this.regionMark = this.buf.cursor();
        this.regionActive = RegionType.CHAR;
        return true;
    }
    
    protected boolean exchangePointAndMark() {
        if (this.count == 0) {
            this.regionActive = RegionType.CHAR;
            return true;
        }
        final int x = this.regionMark;
        this.regionMark = this.buf.cursor();
        this.buf.cursor(x);
        if (this.buf.cursor() > this.buf.length()) {
            this.buf.cursor(this.buf.length());
        }
        if (this.count > 0) {
            this.regionActive = RegionType.CHAR;
        }
        return true;
    }
    
    protected boolean visualMode() {
        if (this.isInViMoveOperation()) {
            this.isArgDigit = true;
            this.forceLine = false;
            return this.forceChar = true;
        }
        if (this.regionActive == RegionType.NONE) {
            this.regionMark = this.buf.cursor();
            this.regionActive = RegionType.CHAR;
        }
        else if (this.regionActive == RegionType.CHAR) {
            this.regionActive = RegionType.NONE;
        }
        else if (this.regionActive == RegionType.LINE) {
            this.regionActive = RegionType.CHAR;
        }
        return true;
    }
    
    protected boolean visualLineMode() {
        if (this.isInViMoveOperation()) {
            this.isArgDigit = true;
            this.forceLine = true;
            this.forceChar = false;
            return true;
        }
        if (this.regionActive == RegionType.NONE) {
            this.regionMark = this.buf.cursor();
            this.regionActive = RegionType.LINE;
        }
        else if (this.regionActive == RegionType.CHAR) {
            this.regionActive = RegionType.LINE;
        }
        else if (this.regionActive == RegionType.LINE) {
            this.regionActive = RegionType.NONE;
        }
        return true;
    }
    
    protected boolean deactivateRegion() {
        this.regionActive = RegionType.NONE;
        return true;
    }
    
    protected boolean whatCursorPosition() {
        this.post = (() -> {
            final AttributedStringBuilder sb = new AttributedStringBuilder();
            if (this.buf.cursor() < this.buf.length()) {
                final int c = this.buf.currChar();
                sb.append("Char: ");
                if (c == 32) {
                    sb.append("SPC");
                }
                else if (c == 10) {
                    sb.append("LFD");
                }
                else if (c < 32) {
                    sb.append('^');
                    sb.append((char)(c + 65 - 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(this.buf.cursor() + 1));
            sb.append(" of ");
            sb.append(Integer.toString(this.buf.length() + 1));
            sb.append(" (");
            sb.append(Integer.toString((this.buf.length() == 0) ? 100 : (100 * this.buf.cursor() / this.buf.length())));
            sb.append("%)");
            sb.append("   ");
            sb.append("column ");
            sb.append(Integer.toString(this.buf.cursor() - this.findbol()));
            return sb.toAttributedString();
        });
        return true;
    }
    
    protected boolean editAndExecute() {
        boolean out = true;
        File file = null;
        try {
            file = File.createTempFile("jline-execute-", null);
            try (final FileWriter writer = new FileWriter(file)) {
                writer.write(this.buf.toString());
            }
            this.editAndAddInBuffer(file);
        }
        catch (final Exception e) {
            e.printStackTrace(this.terminal.writer());
            out = false;
        }
        finally {
            this.state = State.IGNORE;
            if (file != null && file.exists()) {
                file.delete();
            }
        }
        return out;
    }
    
    protected Map<String, Widget> builtinWidgets() {
        final Map<String, Widget> widgets = new HashMap<String, Widget>();
        this.addBuiltinWidget(widgets, "accept-and-infer-next-history", this::acceptAndInferNextHistory);
        this.addBuiltinWidget(widgets, "accept-and-hold", this::acceptAndHold);
        this.addBuiltinWidget(widgets, "accept-line", this::acceptLine);
        this.addBuiltinWidget(widgets, "accept-line-and-down-history", this::acceptLineAndDownHistory);
        this.addBuiltinWidget(widgets, "argument-base", this::argumentBase);
        this.addBuiltinWidget(widgets, "backward-char", this::backwardChar);
        this.addBuiltinWidget(widgets, "backward-delete-char", this::backwardDeleteChar);
        this.addBuiltinWidget(widgets, "backward-delete-word", this::backwardDeleteWord);
        this.addBuiltinWidget(widgets, "backward-kill-line", this::backwardKillLine);
        this.addBuiltinWidget(widgets, "backward-kill-word", this::backwardKillWord);
        this.addBuiltinWidget(widgets, "backward-word", this::backwardWord);
        this.addBuiltinWidget(widgets, "beep", this::beep);
        this.addBuiltinWidget(widgets, "beginning-of-buffer-or-history", this::beginningOfBufferOrHistory);
        this.addBuiltinWidget(widgets, "beginning-of-history", this::beginningOfHistory);
        this.addBuiltinWidget(widgets, "beginning-of-line", this::beginningOfLine);
        this.addBuiltinWidget(widgets, "beginning-of-line-hist", this::beginningOfLineHist);
        this.addBuiltinWidget(widgets, "capitalize-word", this::capitalizeWord);
        this.addBuiltinWidget(widgets, "clear", this::clear);
        this.addBuiltinWidget(widgets, "clear-screen", this::clearScreen);
        this.addBuiltinWidget(widgets, "complete-prefix", this::completePrefix);
        this.addBuiltinWidget(widgets, "complete-word", this::completeWord);
        this.addBuiltinWidget(widgets, "copy-prev-word", this::copyPrevWord);
        this.addBuiltinWidget(widgets, "copy-region-as-kill", this::copyRegionAsKill);
        this.addBuiltinWidget(widgets, "delete-char", this::deleteChar);
        this.addBuiltinWidget(widgets, "delete-char-or-list", this::deleteCharOrList);
        this.addBuiltinWidget(widgets, "delete-word", this::deleteWord);
        this.addBuiltinWidget(widgets, "digit-argument", this::digitArgument);
        this.addBuiltinWidget(widgets, "do-lowercase-version", this::doLowercaseVersion);
        this.addBuiltinWidget(widgets, "down-case-word", this::downCaseWord);
        this.addBuiltinWidget(widgets, "down-line", this::downLine);
        this.addBuiltinWidget(widgets, "down-line-or-history", this::downLineOrHistory);
        this.addBuiltinWidget(widgets, "down-line-or-search", this::downLineOrSearch);
        this.addBuiltinWidget(widgets, "down-history", this::downHistory);
        this.addBuiltinWidget(widgets, "edit-and-execute-command", this::editAndExecute);
        this.addBuiltinWidget(widgets, "emacs-editing-mode", this::emacsEditingMode);
        this.addBuiltinWidget(widgets, "emacs-backward-word", this::emacsBackwardWord);
        this.addBuiltinWidget(widgets, "emacs-forward-word", this::emacsForwardWord);
        this.addBuiltinWidget(widgets, "end-of-buffer-or-history", this::endOfBufferOrHistory);
        this.addBuiltinWidget(widgets, "end-of-history", this::endOfHistory);
        this.addBuiltinWidget(widgets, "end-of-line", this::endOfLine);
        this.addBuiltinWidget(widgets, "end-of-line-hist", this::endOfLineHist);
        this.addBuiltinWidget(widgets, "exchange-point-and-mark", this::exchangePointAndMark);
        this.addBuiltinWidget(widgets, "expand-history", this::expandHistory);
        this.addBuiltinWidget(widgets, "expand-or-complete", this::expandOrComplete);
        this.addBuiltinWidget(widgets, "expand-or-complete-prefix", this::expandOrCompletePrefix);
        this.addBuiltinWidget(widgets, "expand-word", this::expandWord);
        this.addBuiltinWidget(widgets, "fresh-line", this::freshLine);
        this.addBuiltinWidget(widgets, "forward-char", this::forwardChar);
        this.addBuiltinWidget(widgets, "forward-word", this::forwardWord);
        this.addBuiltinWidget(widgets, "history-incremental-search-backward", this::historyIncrementalSearchBackward);
        this.addBuiltinWidget(widgets, "history-incremental-search-forward", this::historyIncrementalSearchForward);
        this.addBuiltinWidget(widgets, "history-search-backward", this::historySearchBackward);
        this.addBuiltinWidget(widgets, "history-search-forward", this::historySearchForward);
        this.addBuiltinWidget(widgets, "insert-close-curly", this::insertCloseCurly);
        this.addBuiltinWidget(widgets, "insert-close-paren", this::insertCloseParen);
        this.addBuiltinWidget(widgets, "insert-close-square", this::insertCloseSquare);
        this.addBuiltinWidget(widgets, "insert-comment", this::insertComment);
        this.addBuiltinWidget(widgets, "kill-buffer", this::killBuffer);
        this.addBuiltinWidget(widgets, "kill-line", this::killLine);
        this.addBuiltinWidget(widgets, "kill-region", this::killRegion);
        this.addBuiltinWidget(widgets, "kill-whole-line", this::killWholeLine);
        this.addBuiltinWidget(widgets, "kill-word", this::killWord);
        this.addBuiltinWidget(widgets, "list-choices", this::listChoices);
        this.addBuiltinWidget(widgets, "menu-complete", this::menuComplete);
        this.addBuiltinWidget(widgets, "menu-expand-or-complete", this::menuExpandOrComplete);
        this.addBuiltinWidget(widgets, "neg-argument", this::negArgument);
        this.addBuiltinWidget(widgets, "overwrite-mode", this::overwriteMode);
        this.addBuiltinWidget(widgets, "quoted-insert", this::quotedInsert);
        this.addBuiltinWidget(widgets, "redisplay", this::redisplay);
        this.addBuiltinWidget(widgets, "redraw-line", this::redrawLine);
        this.addBuiltinWidget(widgets, "redo", this::redo);
        this.addBuiltinWidget(widgets, "self-insert", this::selfInsert);
        this.addBuiltinWidget(widgets, "self-insert-unmeta", this::selfInsertUnmeta);
        this.addBuiltinWidget(widgets, "abort", this::sendBreak);
        this.addBuiltinWidget(widgets, "set-mark-command", this::setMarkCommand);
        this.addBuiltinWidget(widgets, "transpose-chars", this::transposeChars);
        this.addBuiltinWidget(widgets, "transpose-words", this::transposeWords);
        this.addBuiltinWidget(widgets, "undefined-key", this::undefinedKey);
        this.addBuiltinWidget(widgets, "universal-argument", this::universalArgument);
        this.addBuiltinWidget(widgets, "undo", this::undo);
        this.addBuiltinWidget(widgets, "up-case-word", this::upCaseWord);
        this.addBuiltinWidget(widgets, "up-history", this::upHistory);
        this.addBuiltinWidget(widgets, "up-line", this::upLine);
        this.addBuiltinWidget(widgets, "up-line-or-history", this::upLineOrHistory);
        this.addBuiltinWidget(widgets, "up-line-or-search", this::upLineOrSearch);
        this.addBuiltinWidget(widgets, "vi-add-eol", this::viAddEol);
        this.addBuiltinWidget(widgets, "vi-add-next", this::viAddNext);
        this.addBuiltinWidget(widgets, "vi-backward-char", this::viBackwardChar);
        this.addBuiltinWidget(widgets, "vi-backward-delete-char", this::viBackwardDeleteChar);
        this.addBuiltinWidget(widgets, "vi-backward-blank-word", this::viBackwardBlankWord);
        this.addBuiltinWidget(widgets, "vi-backward-blank-word-end", this::viBackwardBlankWordEnd);
        this.addBuiltinWidget(widgets, "vi-backward-kill-word", this::viBackwardKillWord);
        this.addBuiltinWidget(widgets, "vi-backward-word", this::viBackwardWord);
        this.addBuiltinWidget(widgets, "vi-backward-word-end", this::viBackwardWordEnd);
        this.addBuiltinWidget(widgets, "vi-beginning-of-line", this::viBeginningOfLine);
        this.addBuiltinWidget(widgets, "vi-cmd-mode", this::viCmdMode);
        this.addBuiltinWidget(widgets, "vi-digit-or-beginning-of-line", this::viDigitOrBeginningOfLine);
        this.addBuiltinWidget(widgets, "vi-down-line-or-history", this::viDownLineOrHistory);
        this.addBuiltinWidget(widgets, "vi-change-to", this::viChange);
        this.addBuiltinWidget(widgets, "vi-change-eol", this::viChangeEol);
        this.addBuiltinWidget(widgets, "vi-change-whole-line", this::viChangeWholeLine);
        this.addBuiltinWidget(widgets, "vi-delete-char", this::viDeleteChar);
        this.addBuiltinWidget(widgets, "vi-delete", this::viDelete);
        this.addBuiltinWidget(widgets, "vi-end-of-line", this::viEndOfLine);
        this.addBuiltinWidget(widgets, "vi-kill-eol", this::viKillEol);
        this.addBuiltinWidget(widgets, "vi-first-non-blank", this::viFirstNonBlank);
        this.addBuiltinWidget(widgets, "vi-find-next-char", this::viFindNextChar);
        this.addBuiltinWidget(widgets, "vi-find-next-char-skip", this::viFindNextCharSkip);
        this.addBuiltinWidget(widgets, "vi-find-prev-char", this::viFindPrevChar);
        this.addBuiltinWidget(widgets, "vi-find-prev-char-skip", this::viFindPrevCharSkip);
        this.addBuiltinWidget(widgets, "vi-forward-blank-word", this::viForwardBlankWord);
        this.addBuiltinWidget(widgets, "vi-forward-blank-word-end", this::viForwardBlankWordEnd);
        this.addBuiltinWidget(widgets, "vi-forward-char", this::viForwardChar);
        this.addBuiltinWidget(widgets, "vi-forward-word", this::viForwardWord);
        this.addBuiltinWidget(widgets, "vi-forward-word", this::viForwardWord);
        this.addBuiltinWidget(widgets, "vi-forward-word-end", this::viForwardWordEnd);
        this.addBuiltinWidget(widgets, "vi-history-search-backward", this::viHistorySearchBackward);
        this.addBuiltinWidget(widgets, "vi-history-search-forward", this::viHistorySearchForward);
        this.addBuiltinWidget(widgets, "vi-insert", this::viInsert);
        this.addBuiltinWidget(widgets, "vi-insert-bol", this::viInsertBol);
        this.addBuiltinWidget(widgets, "vi-insert-comment", this::viInsertComment);
        this.addBuiltinWidget(widgets, "vi-join", this::viJoin);
        this.addBuiltinWidget(widgets, "vi-kill-line", this::viKillWholeLine);
        this.addBuiltinWidget(widgets, "vi-match-bracket", this::viMatchBracket);
        this.addBuiltinWidget(widgets, "vi-open-line-above", this::viOpenLineAbove);
        this.addBuiltinWidget(widgets, "vi-open-line-below", this::viOpenLineBelow);
        this.addBuiltinWidget(widgets, "vi-put-after", this::viPutAfter);
        this.addBuiltinWidget(widgets, "vi-put-before", this::viPutBefore);
        this.addBuiltinWidget(widgets, "vi-repeat-find", this::viRepeatFind);
        this.addBuiltinWidget(widgets, "vi-repeat-search", this::viRepeatSearch);
        this.addBuiltinWidget(widgets, "vi-replace-chars", this::viReplaceChars);
        this.addBuiltinWidget(widgets, "vi-rev-repeat-find", this::viRevRepeatFind);
        this.addBuiltinWidget(widgets, "vi-rev-repeat-search", this::viRevRepeatSearch);
        this.addBuiltinWidget(widgets, "vi-swap-case", this::viSwapCase);
        this.addBuiltinWidget(widgets, "vi-up-line-or-history", this::viUpLineOrHistory);
        this.addBuiltinWidget(widgets, "vi-yank", this::viYankTo);
        this.addBuiltinWidget(widgets, "vi-yank-whole-line", this::viYankWholeLine);
        this.addBuiltinWidget(widgets, "visual-line-mode", this::visualLineMode);
        this.addBuiltinWidget(widgets, "visual-mode", this::visualMode);
        this.addBuiltinWidget(widgets, "what-cursor-position", this::whatCursorPosition);
        this.addBuiltinWidget(widgets, "yank", this::yank);
        this.addBuiltinWidget(widgets, "yank-pop", this::yankPop);
        this.addBuiltinWidget(widgets, "mouse", this::mouse);
        this.addBuiltinWidget(widgets, "begin-paste", this::beginPaste);
        this.addBuiltinWidget(widgets, "terminal-focus-in", this::focusIn);
        this.addBuiltinWidget(widgets, "terminal-focus-out", this::focusOut);
        return widgets;
    }
    
    private void addBuiltinWidget(final Map<String, Widget> widgets, final String name, final Widget widget) {
        widgets.put(name, this.namedWidget("." + name, widget));
    }
    
    private Widget namedWidget(final String name, final Widget widget) {
        return new Widget() {
            @Override
            public String toString() {
                return name;
            }
            
            @Override
            public boolean apply() {
                return widget.apply();
            }
        };
    }
    
    public boolean redisplay() {
        this.redisplay(true);
        return true;
    }
    
    protected void redisplay(final boolean flush) {
        try {
            this.lock.lock();
            if (this.skipRedisplay) {
                this.skipRedisplay = false;
                return;
            }
            final Status status = Status.getStatus(this.terminal, false);
            if (status != null) {
                status.redraw();
            }
            if (this.size.getRows() > 0 && this.size.getRows() < 3) {
                final AttributedStringBuilder sb = new AttributedStringBuilder().tabs(this.getTabWidth());
                sb.append(this.prompt);
                this.concat(this.getHighlightedBuffer(this.buf.toString()).columnSplitLength(Integer.MAX_VALUE), sb);
                AttributedString full = sb.toAttributedString();
                sb.setLength(0);
                sb.append(this.prompt);
                String line = this.buf.upToCursor();
                if (this.maskingCallback != null) {
                    line = this.maskingCallback.display(line);
                }
                this.concat(new AttributedString(line).columnSplitLength(Integer.MAX_VALUE), sb);
                final AttributedString toCursor = sb.toAttributedString();
                final int w = WCWidth.wcwidth(8230);
                final int width = this.size.getColumns();
                final int cursor = toCursor.columnLength();
                final int inc = width / 2 + 1;
                while (cursor <= this.smallTerminalOffset + w) {
                    this.smallTerminalOffset -= inc;
                }
                while (cursor >= this.smallTerminalOffset + width - w) {
                    this.smallTerminalOffset += inc;
                }
                if (this.smallTerminalOffset > 0) {
                    sb.setLength(0);
                    sb.append("\u2026");
                    sb.append(full.columnSubSequence(this.smallTerminalOffset + w, Integer.MAX_VALUE));
                    full = sb.toAttributedString();
                }
                final int length = full.columnLength();
                if (length >= this.smallTerminalOffset + width) {
                    sb.setLength(0);
                    sb.append(full.columnSubSequence(0, width - w));
                    sb.append("\u2026");
                    full = sb.toAttributedString();
                }
                this.display.update(Collections.singletonList(full), cursor - this.smallTerminalOffset, flush);
                return;
            }
            final List<AttributedString> secondaryPrompts = new ArrayList<AttributedString>();
            AttributedString full = this.getDisplayedBufferWithPrompts(secondaryPrompts);
            List<AttributedString> newLines;
            if (this.size.getColumns() <= 0) {
                newLines = new ArrayList<AttributedString>();
                newLines.add(full);
            }
            else {
                newLines = full.columnSplitLength(this.size.getColumns(), true, this.display.delayLineWrap());
            }
            List<AttributedString> rightPromptLines;
            if (this.rightPrompt.length() == 0 || this.size.getColumns() <= 0) {
                rightPromptLines = new ArrayList<AttributedString>();
            }
            else {
                rightPromptLines = this.rightPrompt.columnSplitLength(this.size.getColumns());
            }
            while (newLines.size() < rightPromptLines.size()) {
                newLines.add(new AttributedString(""));
            }
            for (int i = 0; i < rightPromptLines.size(); ++i) {
                final AttributedString line2 = rightPromptLines.get(i);
                newLines.set(i, this.addRightPrompt(line2, newLines.get(i)));
            }
            int cursorPos = -1;
            int cursorNewLinesId = -1;
            int cursorColPos = -1;
            if (this.size.getColumns() > 0) {
                final AttributedStringBuilder sb2 = new AttributedStringBuilder().tabs(this.getTabWidth());
                sb2.append(this.prompt);
                String buffer = this.buf.upToCursor();
                if (this.maskingCallback != null) {
                    buffer = this.maskingCallback.display(buffer);
                }
                sb2.append(this.insertSecondaryPrompts(new AttributedString(buffer), secondaryPrompts, false));
                final List<AttributedString> promptLines = sb2.columnSplitLength(this.size.getColumns(), false, this.display.delayLineWrap());
                if (!promptLines.isEmpty()) {
                    cursorNewLinesId = promptLines.size() - 1;
                    cursorColPos = promptLines.get(promptLines.size() - 1).columnLength();
                    cursorPos = this.size.cursorPos(cursorNewLinesId, cursorColPos);
                }
            }
            List<AttributedString> newLinesToDisplay = new ArrayList<AttributedString>();
            final int displaySize = this.displayRows(status);
            if (newLines.size() > displaySize && !this.isTerminalDumb()) {
                final StringBuilder sb3 = new StringBuilder(">....");
                for (int j = sb3.toString().length(); j < this.size.getColumns(); ++j) {
                    sb3.append(" ");
                }
                final AttributedString partialCommandInfo = new AttributedString(sb3.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 k = startId; k < endId; ++k) {
                    if (cursorNewLinesId == lineId) {
                        cursorRowPos = k;
                    }
                    newLinesToDisplay.add(newLines.get(lineId++));
                }
                if (startId == 0) {
                    newLinesToDisplay.add(partialCommandInfo);
                }
                cursorPos = this.size.cursorPos(cursorRowPos, cursorColPos);
            }
            else {
                newLinesToDisplay = newLines;
            }
            this.display.update(newLinesToDisplay, cursorPos, flush);
        }
        finally {
            this.lock.unlock();
        }
    }
    
    private void concat(final List<AttributedString> lines, final 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(final String buffer) {
        if (buffer.length() == 0) {
            return "";
        }
        final History history = this.getHistory();
        final StringBuilder sb = new StringBuilder();
        for (final 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);
        }
        final Pattern pattern = Pattern.compile(sb.toString() + ".*", 32);
        final Iterator<History.Entry> iter = history.reverseIterator(history.last());
        String suggestion = "";
        int tot = 0;
        while (iter.hasNext()) {
            final History.Entry entry = iter.next();
            final Matcher matcher = pattern.matcher(entry.line());
            if (matcher.matches()) {
                suggestion = entry.line().substring(buffer.length());
                break;
            }
            if (tot > 200) {
                break;
            }
            ++tot;
        }
        return suggestion;
    }
    
    public AttributedString getDisplayedBufferWithPrompts(final List<AttributedString> secondaryPrompts) {
        final AttributedString attBuf = this.getHighlightedBuffer(this.buf.toString());
        final AttributedString tNewBuf = this.insertSecondaryPrompts(attBuf, secondaryPrompts);
        final AttributedStringBuilder full = new AttributedStringBuilder().tabs(this.getTabWidth());
        full.append(this.prompt);
        full.append(tNewBuf);
        if (this.doAutosuggestion && !this.isTerminalDumb()) {
            final String lastBinding = (this.getLastBinding() != null) ? this.getLastBinding() : "";
            if (this.autosuggestion == SuggestionType.HISTORY) {
                final AttributedStringBuilder sb = new AttributedStringBuilder();
                this.tailTip = this.matchPreviousCommand(this.buf.toString());
                sb.styled(AttributedStyle::faint, this.tailTip);
                full.append(sb.toAttributedString());
            }
            else if (this.autosuggestion == SuggestionType.COMPLETER) {
                if (this.buf.length() >= this.getInt("suggestions-min-buffer-size", 1) && this.buf.length() == this.buf.cursor() && (!lastBinding.equals("\t") || this.buf.prevChar() == 32 || this.buf.prevChar() == 61)) {
                    this.clearChoices();
                    this.listChoices(true);
                }
                else if (!lastBinding.equals("\t")) {
                    this.clearChoices();
                }
            }
            else if (this.autosuggestion == SuggestionType.TAIL_TIP && this.buf.length() == this.buf.cursor()) {
                if (!lastBinding.equals("\t") || this.buf.prevChar() == 32) {
                    this.clearChoices();
                }
                final AttributedStringBuilder sb = new AttributedStringBuilder();
                if (this.buf.prevChar() != 32) {
                    if (!this.tailTip.startsWith("[")) {
                        final int idx = this.tailTip.indexOf(32);
                        final int idb = this.buf.toString().lastIndexOf(32);
                        final int idd = this.buf.toString().lastIndexOf(45);
                        if (idx > 0 && ((idb == -1 && idb == idd) || (idb >= 0 && idb > idd))) {
                            this.tailTip = this.tailTip.substring(idx);
                        }
                        else if (idb >= 0 && idb < idd) {
                            sb.append(" ");
                        }
                    }
                    else {
                        sb.append(" ");
                    }
                }
                sb.styled(AttributedStyle::faint, this.tailTip);
                full.append(sb.toAttributedString());
            }
        }
        if (this.post != null) {
            full.append("\n");
            full.append(this.post.get());
        }
        this.doAutosuggestion = true;
        return full.toAttributedString();
    }
    
    private AttributedString getHighlightedBuffer(String buffer) {
        if (this.maskingCallback != null) {
            buffer = this.maskingCallback.display(buffer);
        }
        if (this.highlighter != null && !this.isSet(Option.DISABLE_HIGHLIGHTER) && buffer.length() < this.getInt("features-max-buffer-size", 1000)) {
            return this.highlighter.highlight(this, buffer);
        }
        return new AttributedString(buffer);
    }
    
    private AttributedString expandPromptPattern(String pattern, int padToWidth, final String message, final int line) {
        final ArrayList<AttributedString> parts = new ArrayList<AttributedString>();
        boolean isHidden = false;
        int padPartIndex = -1;
        StringBuilder padPartString = null;
        StringBuilder sb = new StringBuilder();
        pattern += "%{";
        final int plen = pattern.length();
        int padChar = -1;
        int padPos = -1;
        int cols = 0;
        int i = 0;
        while (i < plen) {
            char ch = pattern.charAt(i++);
            if (ch == '%' && i < plen) {
                int count = 0;
                boolean countSeen = false;
            Label_0608:
                while (true) {
                    ch = pattern.charAt(i++);
                    switch (ch) {
                        case '{':
                        case '}': {
                            final String str = sb.toString();
                            AttributedString astr;
                            if (!isHidden) {
                                astr = this.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();
                            }
                            parts.add(astr);
                            isHidden = (ch == '{');
                            break Label_0608;
                        }
                        case '%': {
                            sb.append(ch);
                            break Label_0608;
                        }
                        case 'N': {
                            sb.append(this.getInt("line-offset", 0) + line);
                            break Label_0608;
                        }
                        case '*': {
                            if (this.currentLine == line) {
                                sb.append("*");
                                break Label_0608;
                            }
                            sb.append(" ");
                            break Label_0608;
                        }
                        case 'M': {
                            if (message != null) {
                                sb.append(message);
                                break Label_0608;
                            }
                            break Label_0608;
                        }
                        case 'P': {
                            if (countSeen && count >= 0) {
                                padToWidth = count;
                            }
                            if (i < plen) {
                                padChar = pattern.charAt(i++);
                            }
                            padPos = sb.length();
                            padPartIndex = parts.size();
                            break Label_0608;
                        }
                        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;
                            continue;
                        }
                        default: {
                            break Label_0608;
                        }
                    }
                }
            }
            else {
                sb.append(ch);
            }
        }
        if (padToWidth > cols) {
            final int padCharCols = WCWidth.wcwidth(padChar);
            int padCount = (padToWidth - cols) / padCharCols;
            sb = padPartString;
            while (--padCount >= 0) {
                sb.insert(padPos, (char)padChar);
            }
            parts.set(padPartIndex, this.fromAnsi(sb.toString()));
        }
        return AttributedString.join(null, parts);
    }
    
    private AttributedString fromAnsi(final String str) {
        return AttributedString.fromAnsi(str, Collections.singletonList(0), this.alternateIn, this.alternateOut);
    }
    
    private AttributedString insertSecondaryPrompts(final AttributedString str, final List<AttributedString> prompts) {
        return this.insertSecondaryPrompts(str, prompts, true);
    }
    
    private AttributedString insertSecondaryPrompts(final AttributedString strAtt, final List<AttributedString> prompts, final boolean computePrompts) {
        Objects.requireNonNull(prompts);
        final List<AttributedString> lines = strAtt.columnSplitLength(Integer.MAX_VALUE);
        final AttributedStringBuilder sb = new AttributedStringBuilder();
        final String secondaryPromptPattern = this.getString("secondary-prompt-pattern", "%M> ");
        final boolean needsMessage = secondaryPromptPattern.contains("%M") && strAtt.length() < this.getInt("features-max-buffer-size", 1000);
        final AttributedStringBuilder buf = new AttributedStringBuilder();
        int width = 0;
        final List<String> missings = new ArrayList<String>();
        if (computePrompts && secondaryPromptPattern.contains("%P")) {
            width = this.prompt.columnLength();
            if (width > this.size.getColumns() || this.prompt.contains('\n')) {
                width = new TerminalLine(this.prompt.toString(), 0, this.size.getColumns()).getEndLine().length();
            }
            for (int line = 0; line < lines.size() - 1; ++line) {
                buf.append(lines.get(line)).append("\n");
                String missing = "";
                if (needsMessage) {
                    try {
                        this.parser.parse(buf.toString(), buf.length(), Parser.ParseContext.SECONDARY_PROMPT);
                    }
                    catch (final EOFError e) {
                        missing = e.getMissing();
                    }
                    catch (final SyntaxError syntaxError) {}
                }
                missings.add(missing);
                final AttributedString prompt = this.expandPromptPattern(secondaryPromptPattern, 0, missing, line + 1);
                width = Math.max(width, prompt.columnLength());
            }
            buf.setLength(0);
        }
        int line = 0;
        this.currentLine = -1;
        final int cursor = this.buf.cursor();
        int start = 0;
        for (int l = 0; l < lines.size(); ++l) {
            final int end = start + lines.get(l).length();
            if (cursor >= start && cursor <= end) {
                this.currentLine = l;
                break;
            }
            start = end + 1;
        }
        while (line < lines.size() - 1) {
            sb.append(lines.get(line)).append("\n");
            buf.append(lines.get(line)).append("\n");
            AttributedString prompt2;
            if (computePrompts) {
                String missing2 = "";
                if (needsMessage) {
                    if (missings.isEmpty()) {
                        try {
                            this.parser.parse(buf.toString(), buf.length(), Parser.ParseContext.SECONDARY_PROMPT);
                        }
                        catch (final EOFError e2) {
                            missing2 = e2.getMissing();
                        }
                        catch (final SyntaxError syntaxError2) {}
                    }
                    else {
                        missing2 = missings.get(line);
                    }
                }
                prompt2 = this.expandPromptPattern(secondaryPromptPattern, width, missing2, line + 1);
            }
            else {
                prompt2 = prompts.get(line);
            }
            prompts.add(prompt2);
            sb.append(prompt2);
            ++line;
        }
        sb.append(lines.get(line));
        buf.append(lines.get(line));
        return sb.toAttributedString();
    }
    
    private AttributedString addRightPrompt(final AttributedString prompt, AttributedString line) {
        final int width = prompt.columnLength();
        final boolean endsWithNl = line.length() > 0 && line.charAt(line.length() - 1) == '\n';
        final int nb = this.size.getColumns() - width - (line.columnLength() + (endsWithNl ? 1 : 0));
        if (nb >= 3) {
            final AttributedStringBuilder sb = new AttributedStringBuilder(this.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;
    }
    
    protected boolean insertTab() {
        return this.isSet(Option.INSERT_TAB) && this.getLastBinding().equals("\t") && this.buf.toString().matches("(^|[\\s\\S]*\n)[\r\n\t ]*");
    }
    
    protected boolean expandHistory() {
        final String str = this.buf.toString();
        final String exp = this.expander.expandHistory(this.history, str);
        if (!exp.equals(str)) {
            this.buf.clear();
            this.buf.write(exp);
            return true;
        }
        return false;
    }
    
    protected boolean expandWord() {
        if (this.insertTab()) {
            return this.selfInsert();
        }
        return this.doComplete(CompletionType.Expand, this.isSet(Option.MENU_COMPLETE), false);
    }
    
    protected boolean expandOrComplete() {
        if (this.insertTab()) {
            return this.selfInsert();
        }
        return this.doComplete(CompletionType.ExpandComplete, this.isSet(Option.MENU_COMPLETE), false);
    }
    
    protected boolean expandOrCompletePrefix() {
        if (this.insertTab()) {
            return this.selfInsert();
        }
        return this.doComplete(CompletionType.ExpandComplete, this.isSet(Option.MENU_COMPLETE), true);
    }
    
    protected boolean completeWord() {
        if (this.insertTab()) {
            return this.selfInsert();
        }
        return this.doComplete(CompletionType.Complete, this.isSet(Option.MENU_COMPLETE), false);
    }
    
    protected boolean menuComplete() {
        if (this.insertTab()) {
            return this.selfInsert();
        }
        return this.doComplete(CompletionType.Complete, true, false);
    }
    
    protected boolean menuExpandOrComplete() {
        if (this.insertTab()) {
            return this.selfInsert();
        }
        return this.doComplete(CompletionType.ExpandComplete, true, false);
    }
    
    protected boolean completePrefix() {
        if (this.insertTab()) {
            return this.selfInsert();
        }
        return this.doComplete(CompletionType.Complete, this.isSet(Option.MENU_COMPLETE), true);
    }
    
    protected boolean listChoices() {
        return this.listChoices(false);
    }
    
    private boolean listChoices(final boolean forSuggestion) {
        return this.doComplete(CompletionType.List, this.isSet(Option.MENU_COMPLETE), false, forSuggestion);
    }
    
    protected boolean deleteCharOrList() {
        if (this.buf.cursor() != this.buf.length() || this.buf.length() == 0) {
            return this.deleteChar();
        }
        return this.doComplete(CompletionType.List, this.isSet(Option.MENU_COMPLETE), false);
    }
    
    protected boolean doComplete(final CompletionType lst, final boolean useMenu, final boolean prefix) {
        return this.doComplete(lst, useMenu, prefix, false);
    }
    
    protected boolean doComplete(CompletionType lst, final boolean useMenu, final boolean prefix, final boolean forSuggestion) {
        if (this.getBoolean("disable-completion", false)) {
            return true;
        }
        if (!this.isSet(Option.DISABLE_EVENT_EXPANSION)) {
            try {
                if (this.expandHistory()) {
                    return true;
                }
            }
            catch (final Exception e) {
                Log.info("Error while expanding history", e);
                return false;
            }
        }
        CompletingParsedLine line;
        try {
            line = wrap(this.parser.parse(this.buf.toString(), this.buf.cursor(), Parser.ParseContext.COMPLETE));
        }
        catch (final Exception e2) {
            Log.info("Error while parsing line", e2);
            return false;
        }
        final List<Candidate> candidates = new ArrayList<Candidate>();
        try {
            if (this.completer != null) {
                this.completer.complete(this, line, candidates);
            }
        }
        catch (final Exception e3) {
            Log.info("Error while finding completion candidates", e3);
            if (Log.isDebugEnabled()) {
                e3.printStackTrace();
            }
            return false;
        }
        if (lst == CompletionType.ExpandComplete || lst == CompletionType.Expand) {
            final String w = this.expander.expandVar(line.word());
            if (!line.word().equals(w)) {
                if (prefix) {
                    this.buf.backspace(line.wordCursor());
                }
                else {
                    this.buf.move(line.word().length() - line.wordCursor());
                    this.buf.backspace(line.word().length());
                }
                this.buf.write(w);
                return true;
            }
            if (lst == CompletionType.Expand) {
                return false;
            }
            lst = CompletionType.Complete;
        }
        final boolean caseInsensitive = this.isSet(Option.CASE_INSENSITIVE);
        final int errors = this.getInt("errors", 2);
        this.completionMatcher.compile(this.options, prefix, line, caseInsensitive, errors, this.getOriginalGroupName());
        final List<Candidate> possible = this.completionMatcher.matches(candidates);
        if (possible.isEmpty()) {
            return false;
        }
        this.size.copy(this.terminal.getSize());
        try {
            if (lst == CompletionType.List) {
                final List<Candidate> possible2 = possible;
                final String word = line.word();
                final boolean runLoop = false;
                final CompletingParsedLine obj = line;
                Objects.requireNonNull(obj);
                this.doList(possible2, word, runLoop, obj::escape, forSuggestion);
                return !possible.isEmpty();
            }
            Candidate completion = null;
            if (possible.size() == 1) {
                completion = possible.get(0);
            }
            else if (this.isSet(Option.RECOGNIZE_EXACT)) {
                completion = this.completionMatcher.exactMatch();
            }
            if (completion != null && !completion.value().isEmpty()) {
                if (prefix) {
                    this.buf.backspace(line.rawWordCursor());
                }
                else {
                    this.buf.move(line.rawWordLength() - line.rawWordCursor());
                    this.buf.backspace(line.rawWordLength());
                }
                this.buf.write(line.escape(completion.value(), completion.complete()));
                if (completion.complete()) {
                    if (this.buf.currChar() != 32) {
                        this.buf.write(" ");
                    }
                    else {
                        this.buf.move(1);
                    }
                }
                if (completion.suffix() != null) {
                    if (this.autosuggestion == SuggestionType.COMPLETER) {
                        this.listChoices(true);
                    }
                    this.redisplay();
                    final Binding op = this.readBinding(this.getKeys());
                    if (op != null) {
                        final String chars = this.getString("REMOVE_SUFFIX_CHARS", " \t\n;&|");
                        final String ref = (op instanceof Reference) ? ((Reference)op).name() : null;
                        if (("self-insert".equals(ref) && chars.indexOf(this.getLastBinding().charAt(0)) >= 0) || "accept-line".equals(ref)) {
                            this.buf.backspace(completion.suffix().length());
                            if (this.getLastBinding().charAt(0) != ' ') {
                                this.buf.write(32);
                            }
                        }
                        this.pushBackBinding(true);
                    }
                }
                return true;
            }
            if (useMenu) {
                this.buf.move(line.word().length() - line.wordCursor());
                this.buf.backspace(line.word().length());
                final List<Candidate> original = possible;
                final String word2 = line.word();
                final CompletingParsedLine obj2 = line;
                Objects.requireNonNull(obj2);
                this.doMenu(original, word2, obj2::escape);
                return true;
            }
            String current;
            if (prefix) {
                current = line.word().substring(0, line.wordCursor());
            }
            else {
                current = line.word();
                this.buf.move(line.rawWordLength() - line.rawWordCursor());
            }
            final String commonPrefix = this.completionMatcher.getCommonPrefix();
            final boolean hasUnambiguous = commonPrefix.startsWith(current) && !commonPrefix.equals(current);
            if (hasUnambiguous) {
                this.buf.backspace(line.rawWordLength());
                this.buf.write(line.escape(commonPrefix, false));
                this.callWidget("redisplay");
                current = commonPrefix;
                if (((!this.isSet(Option.AUTO_LIST) && this.isSet(Option.AUTO_MENU)) || (this.isSet(Option.AUTO_LIST) && this.isSet(Option.LIST_AMBIGUOUS))) && !this.nextBindingIsComplete()) {
                    return true;
                }
            }
            if (this.isSet(Option.AUTO_LIST)) {
                final List<Candidate> possible3 = possible;
                final String completed = current;
                final boolean runLoop2 = true;
                final CompletingParsedLine obj3 = line;
                Objects.requireNonNull(obj3);
                if (!this.doList(possible3, completed, runLoop2, obj3::escape)) {
                    return true;
                }
            }
            if (this.isSet(Option.AUTO_MENU)) {
                this.buf.backspace(current.length());
                final List<Candidate> original2 = possible;
                final String word3 = line.word();
                final CompletingParsedLine obj4 = line;
                Objects.requireNonNull(obj4);
                this.doMenu(original2, word3, obj4::escape);
            }
            return true;
        }
        finally {
            this.size.copy(this.terminal.getBufferSize());
        }
    }
    
    protected static CompletingParsedLine wrap(final ParsedLine line) {
        if (line instanceof CompletingParsedLine) {
            return (CompletingParsedLine)line;
        }
        return new CompletingParsedLine() {
            @Override
            public String word() {
                return line.word();
            }
            
            @Override
            public int wordCursor() {
                return line.wordCursor();
            }
            
            @Override
            public int wordIndex() {
                return line.wordIndex();
            }
            
            @Override
            public List<String> words() {
                return line.words();
            }
            
            @Override
            public String line() {
                return line.line();
            }
            
            @Override
            public int cursor() {
                return line.cursor();
            }
            
            @Override
            public CharSequence escape(final CharSequence candidate, final boolean complete) {
                return candidate;
            }
            
            @Override
            public int rawWordCursor() {
                return this.wordCursor();
            }
            
            @Override
            public int rawWordLength() {
                return this.word().length();
            }
        };
    }
    
    protected Comparator<Candidate> getCandidateComparator(final boolean caseInsensitive, final String word) {
        final String wdi = caseInsensitive ? word.toLowerCase() : word;
        final ToIntFunction<String> wordDistance = w -> ReaderUtils.distance(wdi, caseInsensitive ? w.toLowerCase() : w);
        return Comparator.comparing((Function<? super Candidate, ?>)Candidate::value, Comparator.comparingInt((ToIntFunction<? super Object>)wordDistance)).thenComparing(Comparator.naturalOrder());
    }
    
    protected String getOthersGroupName() {
        return this.getString("OTHERS_GROUP_NAME", "others");
    }
    
    protected String getOriginalGroupName() {
        return this.getString("ORIGINAL_GROUP_NAME", "original");
    }
    
    protected Comparator<String> getGroupComparator() {
        return Comparator.comparingInt(s -> this.getOthersGroupName().equals(s) ? 1 : (this.getOriginalGroupName().equals(s) ? -1 : 0)).thenComparing((Function<? super String, ?>)String::toLowerCase, Comparator.naturalOrder());
    }
    
    private void mergeCandidates(final List<Candidate> possible) {
        final Map<String, List<Candidate>> keyedCandidates = new HashMap<String, List<Candidate>>();
        for (final Candidate candidate : possible) {
            if (candidate.key() != null) {
                final List<Candidate> cands = keyedCandidates.computeIfAbsent(candidate.key(), s -> new ArrayList());
                cands.add(candidate);
            }
        }
        if (!keyedCandidates.isEmpty()) {
            for (final List<Candidate> candidates : keyedCandidates.values()) {
                if (candidates.size() >= 1) {
                    possible.removeAll(candidates);
                    candidates.sort(Comparator.comparing((Function<? super Candidate, ? extends Comparable>)Candidate::value));
                    final Candidate first = candidates.get(0);
                    final String disp = candidates.stream().map((Function<? super Object, ?>)Candidate::displ).collect((Collector<? super Object, ?, String>)Collectors.joining(" "));
                    possible.add(new Candidate(first.value(), disp, first.group(), first.descr(), first.suffix(), null, first.complete()));
                }
            }
        }
    }
    
    protected boolean nextBindingIsComplete() {
        this.redisplay();
        final KeyMap<Binding> keyMap = this.keyMaps.get("menu");
        final Binding operation = this.readBinding(this.getKeys(), keyMap);
        if (operation instanceof Reference && "menu-complete".equals(((Reference)operation).name())) {
            return true;
        }
        this.pushBackBinding();
        return false;
    }
    
    private int displayRows() {
        return this.displayRows(Status.getStatus(this.terminal, false));
    }
    
    private int displayRows(final Status status) {
        return this.size.getRows() - ((status != null) ? status.size() : 0);
    }
    
    private int visibleDisplayRows() {
        final Status status = Status.getStatus(this.terminal, false);
        return this.terminal.getSize().getRows() - ((status != null) ? status.size() : 0);
    }
    
    private int promptLines() {
        final AttributedString text = this.insertSecondaryPrompts(AttributedStringBuilder.append(this.prompt, this.buf.toString()), new ArrayList<AttributedString>());
        return text.columnSplitLength(this.size.getColumns(), false, this.display.delayLineWrap()).size();
    }
    
    protected boolean doMenu(final List<Candidate> original, final String completed, final BiFunction<CharSequence, Boolean, CharSequence> escaper) {
        final List<Candidate> possible = new ArrayList<Candidate>();
        final boolean caseInsensitive = this.isSet(Option.CASE_INSENSITIVE);
        original.sort(this.getCandidateComparator(caseInsensitive, completed));
        this.mergeCandidates(original);
        this.computePost(original, null, possible, completed);
        final boolean defaultAutoGroup = this.isSet(Option.AUTO_GROUP);
        final boolean defaultGroup = this.isSet(Option.GROUP);
        if (!this.isSet(Option.GROUP_PERSIST)) {
            this.option(Option.AUTO_GROUP, false);
            this.option(Option.GROUP, false);
        }
        final MenuSupport menuSupport = new MenuSupport(original, completed, escaper);
        this.post = menuSupport;
        this.callWidget("redisplay");
        final KeyMap<Binding> keyMap = this.keyMaps.get("menu");
        Binding operation;
        while ((operation = this.readBinding(this.getKeys(), keyMap)) != null) {
            final String s;
            final String ref = s = ((operation instanceof Reference) ? ((Reference)operation).name() : "");
            switch (s) {
                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": {
                    this.clearScreen();
                    break;
                }
                default: {
                    final Candidate completion = menuSupport.completion();
                    if (completion.suffix() != null) {
                        final String chars = this.getString("REMOVE_SUFFIX_CHARS", " \t\n;&|");
                        if (("self-insert".equals(ref) && chars.indexOf(this.getLastBinding().charAt(0)) >= 0) || "backward-delete-char".equals(ref)) {
                            this.buf.backspace(completion.suffix().length());
                        }
                    }
                    if (completion.complete() && this.getLastBinding().charAt(0) != ' ' && ("self-insert".equals(ref) || this.getLastBinding().charAt(0) != ' ')) {
                        this.buf.write(32);
                    }
                    if (!"accept-line".equals(ref) && (!"self-insert".equals(ref) || completion.suffix() == null || !completion.suffix().startsWith(this.getLastBinding()))) {
                        this.pushBackBinding(true);
                    }
                    this.post = null;
                    this.option(Option.AUTO_GROUP, defaultAutoGroup);
                    this.option(Option.GROUP, defaultGroup);
                    return true;
                }
            }
            this.doAutosuggestion = false;
            this.callWidget("redisplay");
        }
        this.option(Option.AUTO_GROUP, defaultAutoGroup);
        this.option(Option.GROUP, defaultGroup);
        return false;
    }
    
    protected boolean clearChoices() {
        return this.doList(new ArrayList<Candidate>(), "", false, null, false);
    }
    
    protected boolean doList(final List<Candidate> possible, final String completed, final boolean runLoop, final BiFunction<CharSequence, Boolean, CharSequence> escaper) {
        return this.doList(possible, completed, runLoop, escaper, false);
    }
    
    protected boolean doList(final List<Candidate> possible, final String completed, final boolean runLoop, final BiFunction<CharSequence, Boolean, CharSequence> escaper, final boolean forSuggestion) {
        this.mergeCandidates(possible);
        final AttributedString text = this.insertSecondaryPrompts(AttributedStringBuilder.append(this.prompt, this.buf.toString()), new ArrayList<AttributedString>());
        final int promptLines = text.columnSplitLength(this.size.getColumns(), false, this.display.delayLineWrap()).size();
        final PostResult postResult = this.computePost(possible, null, null, completed);
        final int lines = postResult.lines;
        final int listMax = this.getInt("list-max", 100);
        final int possibleSize = possible.size();
        if (possibleSize == 0 || this.size.getRows() == 0) {
            return false;
        }
        if ((listMax > 0 && possibleSize >= listMax) || lines >= this.size.getRows() - promptLines) {
            if (forSuggestion) {
                return false;
            }
            this.post = (Supplier<AttributedString>)(() -> {
                new AttributedString(this.getAppName() + ": do you wish to see all " + possibleSize + " possibilities (" + lines + " lines)?");
                return;
            });
            this.redisplay(true);
            final int c = this.readCharacter();
            if (c != 121 && c != 89 && c != 9) {
                this.post = null;
                return false;
            }
        }
        final boolean caseInsensitive = this.isSet(Option.CASE_INSENSITIVE);
        final StringBuilder sb = new StringBuilder();
        this.candidateStartPosition = 0;
        while (true) {
            final String current = completed + sb.toString();
            List<Candidate> cands;
            if (sb.length() > 0) {
                this.completionMatcher.compile(this.options, false, new CompletingWord(current), caseInsensitive, 0, null);
                cands = this.completionMatcher.matches(possible).stream().sorted((Comparator<? super Object>)this.getCandidateComparator(caseInsensitive, current)).collect((Collector<? super Object, ?, List<Candidate>>)Collectors.toList());
            }
            else {
                cands = possible.stream().sorted((Comparator<? super Object>)this.getCandidateComparator(caseInsensitive, current)).collect((Collector<? super Object, ?, List<Candidate>>)Collectors.toList());
            }
            if (this.isSet(Option.AUTO_MENU_LIST) && this.candidateStartPosition == 0) {
                this.candidateStartPosition = this.candidateStartPosition(cands);
            }
            this.post = (() -> {
                final AttributedString t = this.insertSecondaryPrompts(AttributedStringBuilder.append(this.prompt, this.buf.toString()), new ArrayList<AttributedString>());
                final int pl = t.columnSplitLength(this.size.getColumns(), false, this.display.delayLineWrap()).size();
                final PostResult pr = this.computePost(cands, null, null, current);
                if (pr.lines >= this.size.getRows() - pl) {
                    this.post = null;
                    final int oldCursor = this.buf.cursor();
                    this.buf.cursor(this.buf.length());
                    this.redisplay(false);
                    this.buf.cursor(oldCursor);
                    this.println();
                    final List<AttributedString> ls = pr.post.columnSplitLength(this.size.getColumns(), false, this.display.delayLineWrap());
                    final Display d = new Display(this.terminal, false);
                    d.resize(this.size.getRows(), this.size.getColumns());
                    d.update(ls, -1);
                    this.println();
                    this.redrawLine();
                    return new AttributedString("");
                }
                else {
                    return pr.post;
                }
            });
            if (!runLoop) {
                return false;
            }
            this.redisplay();
            final Binding b = this.doReadBinding(this.getKeys(), null);
            if (b instanceof Reference) {
                final String name = ((Reference)b).name();
                if ("backward-delete-char".equals(name) || "vi-backward-delete-char".equals(name)) {
                    if (sb.length() == 0) {
                        this.pushBackBinding();
                        this.post = null;
                        return false;
                    }
                    sb.setLength();
                    this.buf.backspace();
                }
                else if ("self-insert".equals(name)) {
                    sb.append(this.getLastBinding());
                    this.callWidget(name);
                    if (cands.isEmpty()) {
                        this.post = null;
                        return false;
                    }
                    continue;
                }
                else {
                    if ("\t".equals(this.getLastBinding())) {
                        if (cands.size() == 1 || sb.length() > 0) {
                            this.post = null;
                            this.pushBackBinding();
                        }
                        else if (this.isSet(Option.AUTO_MENU)) {
                            this.buf.backspace(escaper.apply(current, false).length());
                            this.doMenu(cands, current, escaper);
                        }
                        return false;
                    }
                    this.pushBackBinding();
                    this.post = null;
                    return false;
                }
            }
            else {
                if (b == null) {
                    this.post = null;
                    return false;
                }
                continue;
            }
        }
    }
    
    protected PostResult computePost(final List<Candidate> possible, final Candidate selection, final List<Candidate> ordered, final String completed) {
        final Display display = this.display;
        Objects.requireNonNull(display);
        return this.computePost(possible, selection, ordered, completed, display::wcwidth, this.size.getColumns(), this.isSet(Option.AUTO_GROUP), this.isSet(Option.GROUP), this.isSet(Option.LIST_ROWS_FIRST));
    }
    
    protected PostResult computePost(final List<Candidate> possible, final Candidate selection, final List<Candidate> ordered, final String completed, final Function<String, Integer> wcwidth, final int width, final boolean autoGroup, final boolean groupName, final boolean rowsFirst) {
        final List<Object> strings = new ArrayList<Object>();
        if (groupName) {
            final Comparator<String> groupComparator = this.getGroupComparator();
            final Map<String, List<Candidate>> sorted = (Map<String, List<Candidate>>)((groupComparator != null) ? new TreeMap<Object, Object>(groupComparator) : new LinkedHashMap<Object, Object>());
            for (final Candidate cand : possible) {
                final String group = cand.group();
                sorted.computeIfAbsent((group != null) ? group : "", s -> new ArrayList()).add(cand);
            }
            for (final Map.Entry<String, List<Candidate>> entry : sorted.entrySet()) {
                String group = entry.getKey();
                if (group.isEmpty() && sorted.size() > 1) {
                    group = this.getOthersGroupName();
                }
                if (!group.isEmpty() && autoGroup) {
                    strings.add(group);
                }
                final List<Candidate> candidates = entry.getValue();
                Collections.sort(candidates);
                strings.add(candidates);
                if (ordered != null) {
                    ordered.addAll(candidates);
                }
            }
        }
        else {
            final Set<String> groups = new LinkedHashSet<String>();
            final List<Candidate> sorted2 = new ArrayList<Candidate>();
            for (final Candidate cand : possible) {
                final String group = cand.group();
                if (group != null) {
                    groups.add(group);
                }
                sorted2.add(cand);
            }
            if (autoGroup) {
                strings.addAll(groups);
            }
            Collections.sort(sorted2);
            strings.add(sorted2);
            if (ordered != null) {
                ordered.addAll(sorted2);
            }
        }
        return this.toColumns(strings, selection, completed, wcwidth, width, rowsFirst);
    }
    
    private int candidateStartPosition(final List<Candidate> cands) {
        final List<String> values = cands.stream().map(c -> AttributedString.stripAnsi(c.displ())).filter(c -> !c.matches("\\w+") && c.length() > 1).collect((Collector<? super Object, ?, List<String>>)Collectors.toList());
        final Set<String> notDelimiters = new HashSet<String>();
        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))));
        final int width = this.size.getColumns();
        int promptLength = (this.prompt != null) ? this.prompt.length() : 0;
        if (promptLength > 0) {
            final TerminalLine tp = new TerminalLine(this.prompt.toString(), 0, width);
            promptLength = tp.getEndLine().length();
        }
        final TerminalLine tl = new TerminalLine(this.buf.substring(0, this.buf.cursor()), promptLength, width);
        int out = tl.getStartPos();
        final 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;
    }
    
    protected PostResult toColumns(final List<Object> items, final Candidate selection, final String completed, final Function<String, Integer> wcwidth, int width, final boolean rowsFirst) {
        final int[] out = new int[2];
        int maxWidth = 0;
        int listSize = 0;
        for (final Object item : items) {
            if (item instanceof String) {
                final int len = wcwidth.apply((String)item);
                maxWidth = Math.max(maxWidth, len);
            }
            else {
                if (!(item instanceof List)) {
                    continue;
                }
                for (final Candidate cand : (List)item) {
                    ++listSize;
                    int len2 = wcwidth.apply(cand.displ());
                    if (cand.descr() != null) {
                        len2 = ++len2 + "(".length();
                        len2 += wcwidth.apply(cand.descr());
                        len2 += ")".length();
                    }
                    maxWidth = Math.max(maxWidth, len2);
                }
            }
        }
        final AttributedStringBuilder sb = new AttributedStringBuilder();
        if (listSize > 0) {
            if (this.isSet(Option.AUTO_MENU_LIST) && listSize < Math.min(this.getInt("menu-list-max", Integer.MAX_VALUE), this.visibleDisplayRows() - this.promptLines())) {
                maxWidth = Math.max(maxWidth, 25);
                sb.tabs(Math.max(Math.min(this.candidateStartPosition, width - maxWidth - 1), 1));
                width = maxWidth + 2;
                if (!this.isSet(Option.GROUP_PERSIST)) {
                    List<Candidate> list = new ArrayList<Candidate>();
                    for (final Object o : items) {
                        if (o instanceof Collection) {
                            list.addAll((Collection<? extends Candidate>)o);
                        }
                    }
                    list = list.stream().sorted((Comparator<? super Object>)this.getCandidateComparator(this.isSet(Option.CASE_INSENSITIVE), "")).collect((Collector<? super Object, ?, List<Candidate>>)Collectors.toList());
                    this.toColumns(list, width, maxWidth, sb, selection, completed, rowsFirst, true, out);
                }
                else {
                    for (final Object list2 : items) {
                        this.toColumns(list2, width, maxWidth, sb, selection, completed, rowsFirst, true, out);
                    }
                }
            }
            else {
                for (final Object list2 : items) {
                    this.toColumns(list2, 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]);
    }
    
    protected void toColumns(final Object items, final int width, int maxWidth, final AttributedStringBuilder sb, final Candidate selection, final String completed, final boolean rowsFirst, final boolean doMenuList, final int[] out) {
        if (maxWidth <= 0 || width <= 0) {
            return;
        }
        if (items instanceof String) {
            if (doMenuList) {
                sb.style(AttributedStyle.DEFAULT);
                sb.append('\t');
            }
            final AttributedStringBuilder asb = new AttributedStringBuilder();
            asb.style(this.getCompletionStyleGroup(doMenuList)).append((CharSequence)items).style(AttributedStyle.DEFAULT);
            if (doMenuList) {
                for (int k = ((String)items).length(); k < maxWidth + 1; ++k) {
                    asb.append(' ');
                }
            }
            sb.style(this.getCompletionStyleBackground(doMenuList));
            sb.append(asb);
            sb.append("\n");
            final int n = 0;
            ++out[n];
        }
        else if (items instanceof List) {
            final List<Candidate> candidates = (List<Candidate>)items;
            int c;
            for (maxWidth = Math.min(width, maxWidth), c = width / maxWidth; c > 1 && c * maxWidth + (c - 1) * 3 >= width; --c) {}
            final int lines = (candidates.size() + c - 1) / c;
            final int columns = (candidates.size() + lines - 1) / lines;
            IntBinaryOperator index;
            if (rowsFirst) {
                final int i;
                final int j;
                index = ((i, j) -> i * columns + j);
            }
            else {
                final int i;
                final int j;
                index = ((i, j) -> j * lines + i);
            }
            for (int i = 0; i < lines; ++i) {
                if (doMenuList) {
                    sb.style(AttributedStyle.DEFAULT);
                    sb.append('\t');
                }
                final AttributedStringBuilder asb2 = new AttributedStringBuilder();
                for (int j = 0; j < columns; ++j) {
                    final int idx = index.applyAsInt(i, j);
                    if (idx < candidates.size()) {
                        final Candidate cand = candidates.get(idx);
                        final boolean hasRightItem = j < columns - 1 && index.applyAsInt(i, j + 1) < candidates.size();
                        final AttributedString left = this.fromAnsi(cand.displ());
                        AttributedString right = this.fromAnsi(cand.descr());
                        final int lw = left.columnLength();
                        int rw = 0;
                        if (right != null) {
                            final int rem = maxWidth - (lw + 1 + "(".length() + ")".length());
                            rw = right.columnLength();
                            if (rw > rem) {
                                right = AttributedStringBuilder.append(right.columnSubSequence(0, rem - WCWidth.wcwidth(8230)), "\u2026");
                                rw = right.columnLength();
                            }
                            right = AttributedStringBuilder.append("(", right, ")");
                            rw += "(".length() + ")".length();
                        }
                        if (cand == selection) {
                            out[1] = i;
                            asb2.style(this.getCompletionStyleSelection(doMenuList));
                            if (left.toString().regionMatches(this.isSet(Option.CASE_INSENSITIVE), 0, completed, 0, completed.length())) {
                                asb2.append(left.toString(), 0, completed.length());
                                asb2.append(left.toString(), completed.length(), left.length());
                            }
                            else {
                                asb2.append(left.toString());
                            }
                            for (int l = 0; l < maxWidth - lw - rw; ++l) {
                                asb2.append(' ');
                            }
                            if (right != null) {
                                asb2.append(right);
                            }
                            asb2.style(AttributedStyle.DEFAULT);
                        }
                        else {
                            if (left.toString().regionMatches(this.isSet(Option.CASE_INSENSITIVE), 0, completed, 0, completed.length())) {
                                asb2.style(this.getCompletionStyleStarting(doMenuList));
                                asb2.append(left, 0, completed.length());
                                asb2.style(AttributedStyle.DEFAULT);
                                asb2.append(left, completed.length(), left.length());
                            }
                            else {
                                asb2.append(left);
                            }
                            if (right != null || hasRightItem) {
                                for (int l = 0; l < maxWidth - lw - rw; ++l) {
                                    asb2.append(' ');
                                }
                            }
                            if (right != null) {
                                asb2.style(this.getCompletionStyleDescription(doMenuList));
                                asb2.append(right);
                                asb2.style(AttributedStyle.DEFAULT);
                            }
                            else if (doMenuList) {
                                for (int l = lw; l < maxWidth; ++l) {
                                    asb2.append(' ');
                                }
                            }
                        }
                        if (hasRightItem) {
                            for (int l = 0; l < 3; ++l) {
                                asb2.append(' ');
                            }
                        }
                        if (doMenuList) {
                            asb2.append(' ');
                        }
                    }
                }
                sb.style(this.getCompletionStyleBackground(doMenuList));
                sb.append(asb2);
                sb.append('\n');
            }
            final int n2 = 0;
            out[n2] += lines;
        }
    }
    
    protected AttributedStyle getCompletionStyleStarting(final boolean menuList) {
        return menuList ? this.getCompletionStyleListStarting() : this.getCompletionStyleStarting();
    }
    
    protected AttributedStyle getCompletionStyleDescription(final boolean menuList) {
        return menuList ? this.getCompletionStyleListDescription() : this.getCompletionStyleDescription();
    }
    
    protected AttributedStyle getCompletionStyleGroup(final boolean menuList) {
        return menuList ? this.getCompletionStyleListGroup() : this.getCompletionStyleGroup();
    }
    
    protected AttributedStyle getCompletionStyleSelection(final boolean menuList) {
        return menuList ? this.getCompletionStyleListSelection() : this.getCompletionStyleSelection();
    }
    
    protected AttributedStyle getCompletionStyleBackground(final boolean menuList) {
        return menuList ? this.getCompletionStyleListBackground() : this.getCompletionStyleBackground();
    }
    
    protected AttributedStyle getCompletionStyleStarting() {
        return this.getCompletionStyle("COMPLETION_STYLE_STARTING", "fg:cyan");
    }
    
    protected AttributedStyle getCompletionStyleDescription() {
        return this.getCompletionStyle("COMPLETION_STYLE_DESCRIPTION", "fg:bright-black");
    }
    
    protected AttributedStyle getCompletionStyleGroup() {
        return this.getCompletionStyle("COMPLETION_STYLE_GROUP", "fg:bright-magenta,bold");
    }
    
    protected AttributedStyle getCompletionStyleSelection() {
        return this.getCompletionStyle("COMPLETION_STYLE_SELECTION", "inverse");
    }
    
    protected AttributedStyle getCompletionStyleBackground() {
        return this.getCompletionStyle("COMPLETION_STYLE_BACKGROUND", "bg:default");
    }
    
    protected AttributedStyle getCompletionStyleListStarting() {
        return this.getCompletionStyle("COMPLETION_STYLE_LIST_STARTING", "fg:cyan");
    }
    
    protected AttributedStyle getCompletionStyleListDescription() {
        return this.getCompletionStyle("COMPLETION_STYLE_LIST_DESCRIPTION", "fg:bright-black");
    }
    
    protected AttributedStyle getCompletionStyleListGroup() {
        return this.getCompletionStyle("COMPLETION_STYLE_LIST_GROUP", "fg:black,bold");
    }
    
    protected AttributedStyle getCompletionStyleListSelection() {
        return this.getCompletionStyle("COMPLETION_STYLE_LIST_SELECTION", "inverse");
    }
    
    protected AttributedStyle getCompletionStyleListBackground() {
        return this.getCompletionStyle("COMPLETION_STYLE_LIST_BACKGROUND", "bg:bright-magenta");
    }
    
    protected AttributedStyle getCompletionStyle(final String name, final String value) {
        return new StyleResolver(s -> this.getString(s, null)).resolve("." + name, value);
    }
    
    protected AttributedStyle buildStyle(final String str) {
        return this.fromAnsi("\u001b[" + str + "m ").styleAt(0);
    }
    
    protected boolean moveHistory(final boolean next, final int count) {
        boolean ok = true;
        for (int i = 0; i < count && (ok = this.moveHistory(next)); ++i) {}
        return ok;
    }
    
    protected boolean moveHistory(final boolean next) {
        if (!this.buf.toString().equals(this.history.current())) {
            this.modifiedHistory.put(this.history.index(), this.buf.toString());
        }
        if (next && !this.history.next()) {
            return false;
        }
        if (!next && !this.history.previous()) {
            return false;
        }
        this.setBuffer(this.modifiedHistory.containsKey(this.history.index()) ? this.modifiedHistory.get(this.history.index()) : this.history.current());
        return true;
    }
    
    void print(final String str) {
        this.terminal.writer().write(str);
    }
    
    void println(final String s) {
        this.print(s);
        this.println();
    }
    
    void println() {
        this.terminal.puts(InfoCmp.Capability.carriage_return, new Object[0]);
        this.print("\n");
        this.redrawLine();
    }
    
    protected boolean killBuffer() {
        this.killRing.add(this.buf.toString());
        this.buf.clear();
        return true;
    }
    
    protected boolean killWholeLine() {
        if (this.buf.length() == 0) {
            return false;
        }
        int end;
        int start;
        if (this.count < 0) {
            for (end = this.buf.cursor(); this.buf.atChar(end) != 0 && this.buf.atChar(end) != 10; ++end) {}
            start = end;
            for (int count = -this.count; count > 0; --count) {
                while (start > 0 && this.buf.atChar(start - 1) != 10) {
                    --start;
                }
                --start;
            }
        }
        else {
            for (start = this.buf.cursor(); start > 0 && this.buf.atChar(start - 1) != 10; --start) {}
            end = start;
            while (this.count-- > 0) {
                while (end < this.buf.length() && this.buf.atChar(end) != 10) {
                    ++end;
                }
                if (end < this.buf.length()) {
                    ++end;
                }
            }
        }
        final String killed = this.buf.substring(start, end);
        this.buf.cursor(start);
        this.buf.delete(end - start);
        this.killRing.add(killed);
        return true;
    }
    
    public boolean killLine() {
        if (this.count < 0) {
            return this.callNeg(this::backwardKillLine);
        }
        if (this.buf.cursor() == this.buf.length()) {
            return false;
        }
        int len;
        final int cp = len = this.buf.cursor();
        while (this.count-- > 0) {
            if (this.buf.atChar(len) == 10) {
                ++len;
            }
            else {
                while (this.buf.atChar(len) != 0 && this.buf.atChar(len) != 10) {
                    ++len;
                }
            }
        }
        final int num = len - cp;
        final String killed = this.buf.substring(cp, cp + num);
        this.buf.delete(num);
        this.killRing.add(killed);
        return true;
    }
    
    public boolean backwardKillLine() {
        if (this.count < 0) {
            return this.callNeg(this::killLine);
        }
        if (this.buf.cursor() == 0) {
            return false;
        }
        int beg;
        int cp;
        for (cp = (beg = this.buf.cursor()); this.count-- > 0 && beg != 0; --beg) {
            if (this.buf.atChar(beg - 1) != 10) {
                while (beg > 0 && this.buf.atChar(beg - 1) != 0 && this.buf.atChar(beg - 1) != 10) {
                    --beg;
                }
            }
        }
        final int num = cp - beg;
        final String killed = this.buf.substring(cp - beg, cp);
        this.buf.cursor(beg);
        this.buf.delete(num);
        this.killRing.add(killed);
        return true;
    }
    
    public boolean killRegion() {
        return this.doCopyKillRegion(true);
    }
    
    public boolean copyRegionAsKill() {
        return this.doCopyKillRegion(false);
    }
    
    private boolean doCopyKillRegion(final boolean kill) {
        if (this.regionMark > this.buf.length()) {
            this.regionMark = this.buf.length();
        }
        if (this.regionActive == RegionType.LINE) {
            int start = this.regionMark;
            int end = this.buf.cursor();
            if (start < end) {
                while (start > 0 && this.buf.atChar(start - 1) != 10) {
                    --start;
                }
                while (end < this.buf.length() - 1 && this.buf.atChar(end + 1) != 10) {
                    ++end;
                }
                if (this.isInViCmdMode()) {
                    ++end;
                }
                this.killRing.add(this.buf.substring(start, end));
                if (kill) {
                    this.buf.backspace(end - start);
                }
            }
            else {
                while (end > 0 && this.buf.atChar(end - 1) != 10) {
                    --end;
                }
                while (start < this.buf.length() && this.buf.atChar(start) != 10) {
                    ++start;
                }
                if (this.isInViCmdMode()) {
                    ++start;
                }
                this.killRing.addBackwards(this.buf.substring(end, start));
                if (kill) {
                    this.buf.cursor(end);
                    this.buf.delete(start - end);
                }
            }
        }
        else if (this.regionMark > this.buf.cursor()) {
            if (this.isInViCmdMode()) {
                ++this.regionMark;
            }
            this.killRing.add(this.buf.substring(this.buf.cursor(), this.regionMark));
            if (kill) {
                this.buf.delete(this.regionMark - this.buf.cursor());
            }
        }
        else {
            if (this.isInViCmdMode()) {
                this.buf.move(1);
            }
            this.killRing.add(this.buf.substring(this.regionMark, this.buf.cursor()));
            if (kill) {
                this.buf.backspace(this.buf.cursor() - this.regionMark);
            }
        }
        if (kill) {
            this.regionActive = RegionType.NONE;
        }
        return true;
    }
    
    public boolean yank() {
        final String yanked = this.killRing.yank();
        if (yanked == null) {
            return false;
        }
        this.putString(yanked);
        return true;
    }
    
    public boolean yankPop() {
        if (!this.killRing.lastYank()) {
            return false;
        }
        final String current = this.killRing.yank();
        if (current == null) {
            return false;
        }
        this.buf.backspace(current.length());
        final String yanked = this.killRing.yankPop();
        if (yanked == null) {
            return false;
        }
        this.putString(yanked);
        return true;
    }
    
    public boolean mouse() {
        final MouseEvent event = this.readMouseEvent();
        if (event.getType() == MouseEvent.Type.Released && event.getButton() == MouseEvent.Button.Button1) {
            final StringBuilder tsb = new StringBuilder();
            final Cursor cursor = this.terminal.getCursorPosition(c -> tsb.append((char)c));
            this.bindingReader.runMacro(tsb.toString());
            final List<AttributedString> secondaryPrompts = new ArrayList<AttributedString>();
            this.getDisplayedBufferWithPrompts(secondaryPrompts);
            final AttributedStringBuilder sb = new AttributedStringBuilder().tabs(this.getTabWidth());
            sb.append(this.prompt);
            sb.append(this.insertSecondaryPrompts(new AttributedString(this.buf.upToCursor()), secondaryPrompts, false));
            final List<AttributedString> promptLines = sb.columnSplitLength(this.size.getColumns(), false, this.display.delayLineWrap());
            final int currentLine = promptLines.size() - 1;
            final int wantedLine = Math.max(0, Math.min(currentLine + event.getY() - cursor.getY(), secondaryPrompts.size()));
            final int pl0 = (currentLine == 0) ? this.prompt.columnLength() : secondaryPrompts.get(currentLine - 1).columnLength();
            final int pl2 = (wantedLine == 0) ? this.prompt.columnLength() : secondaryPrompts.get(wantedLine - 1).columnLength();
            final int adjust = pl2 - pl0;
            this.buf.moveXY(event.getX() - cursor.getX() - adjust, event.getY() - cursor.getY());
        }
        return true;
    }
    
    public boolean beginPaste() {
        final String str = this.doReadStringUntil("\u001b[201~");
        this.regionActive = RegionType.PASTE;
        this.regionMark = this.getBuffer().cursor();
        this.getBuffer().write(str.replace('\r', '\n'));
        return true;
    }
    
    public boolean focusIn() {
        return false;
    }
    
    public boolean focusOut() {
        return false;
    }
    
    public boolean clear() {
        this.display.update(Collections.emptyList(), 0);
        return true;
    }
    
    public boolean clearScreen() {
        if (this.terminal.puts(InfoCmp.Capability.clear_screen, new Object[0])) {
            if ("windows-conemu".equals(this.terminal.getType()) && !Boolean.getBoolean("org.jline.terminal.conemu.disable-activate")) {
                this.terminal.writer().write("\u001b[9999E");
            }
            final Status status = Status.getStatus(this.terminal, false);
            if (status != null) {
                status.reset();
            }
            this.redrawLine();
        }
        else {
            this.println();
        }
        return true;
    }
    
    public boolean beep() {
        BellType bell_preference = BellType.AUDIBLE;
        final String lowerCase = this.getString("bell-style", "").toLowerCase();
        switch (lowerCase) {
            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 = (this.getBoolean("prefer-visible-bell", false) ? BellType.VISIBLE : BellType.AUDIBLE);
                break;
            }
        }
        if (bell_preference == BellType.VISIBLE) {
            if (this.terminal.puts(InfoCmp.Capability.flash_screen, new Object[0]) || this.terminal.puts(InfoCmp.Capability.bell, new Object[0])) {
                this.flush();
            }
        }
        else if (bell_preference == BellType.AUDIBLE && this.terminal.puts(InfoCmp.Capability.bell, new Object[0])) {
            this.flush();
        }
        return true;
    }
    
    protected boolean isDelimiter(final int c) {
        return !Character.isLetterOrDigit(c);
    }
    
    protected boolean isWhitespace(final int c) {
        return Character.isWhitespace(c);
    }
    
    protected boolean isViAlphaNum(final int c) {
        return c == 95 || Character.isLetterOrDigit(c);
    }
    
    protected boolean isAlpha(final int c) {
        return Character.isLetter(c);
    }
    
    protected boolean isWord(final int c) {
        final String wordchars = this.getString("WORDCHARS", "*?_-.[]~=/&;!#$%^(){}<>");
        return Character.isLetterOrDigit(c) || (c < 128 && wordchars.indexOf((char)c) >= 0);
    }
    
    String getString(final String name, final String def) {
        return ReaderUtils.getString(this, name, def);
    }
    
    boolean getBoolean(final String name, final boolean def) {
        return ReaderUtils.getBoolean(this, name, def);
    }
    
    int getInt(final String name, final int def) {
        return ReaderUtils.getInt(this, name, def);
    }
    
    long getLong(final String name, final long def) {
        return ReaderUtils.getLong(this, name, def);
    }
    
    @Override
    public Map<String, KeyMap<Binding>> defaultKeyMaps() {
        final Map<String, KeyMap<Binding>> keyMaps = new HashMap<String, KeyMap<Binding>>();
        keyMaps.put("emacs", this.emacs());
        keyMaps.put("vicmd", this.viCmd());
        keyMaps.put("viins", this.viInsertion());
        keyMaps.put("menu", this.menu());
        keyMaps.put("viopp", this.viOpp());
        keyMaps.put("visual", this.visual());
        keyMaps.put(".safe", this.safe());
        keyMaps.put("dumb", this.dumb());
        if (this.getBoolean("bind-tty-special-chars", true)) {
            final Attributes attr = this.terminal.getAttributes();
            this.bindConsoleChars(keyMaps.get("emacs"), attr);
            this.bindConsoleChars(keyMaps.get("viins"), attr);
        }
        for (final KeyMap<Binding> keyMap : keyMaps.values()) {
            keyMap.setUnicode(new Reference("self-insert"));
            keyMap.setAmbiguousTimeout(this.getLong("ambiguous-binding", 1000L));
        }
        keyMaps.put("main", keyMaps.get(this.isTerminalDumb() ? "dumb" : "emacs"));
        return keyMaps;
    }
    
    public KeyMap<Binding> emacs() {
        final KeyMap<Binding> emacs = new KeyMap<Binding>();
        this.bindKeys(emacs);
        this.bind(emacs, "set-mark-command", KeyMap.ctrl('@'));
        this.bind(emacs, "beginning-of-line", KeyMap.ctrl('A'));
        this.bind(emacs, "backward-char", KeyMap.ctrl('B'));
        this.bind(emacs, "delete-char-or-list", KeyMap.ctrl('D'));
        this.bind(emacs, "end-of-line", KeyMap.ctrl('E'));
        this.bind(emacs, "forward-char", KeyMap.ctrl('F'));
        this.bind(emacs, "abort", KeyMap.ctrl('G'));
        this.bind(emacs, "backward-delete-char", KeyMap.ctrl('H'));
        this.bind(emacs, "expand-or-complete", KeyMap.ctrl('I'));
        this.bind(emacs, "accept-line", KeyMap.ctrl('J'));
        this.bind(emacs, "kill-line", KeyMap.ctrl('K'));
        this.bind(emacs, "clear-screen", KeyMap.ctrl('L'));
        this.bind(emacs, "accept-line", KeyMap.ctrl('M'));
        this.bind(emacs, "down-line-or-history", KeyMap.ctrl('N'));
        this.bind(emacs, "accept-line-and-down-history", KeyMap.ctrl('O'));
        this.bind(emacs, "up-line-or-history", KeyMap.ctrl('P'));
        this.bind(emacs, "history-incremental-search-backward", KeyMap.ctrl('R'));
        this.bind(emacs, "history-incremental-search-forward", KeyMap.ctrl('S'));
        this.bind(emacs, "transpose-chars", KeyMap.ctrl('T'));
        this.bind(emacs, "kill-whole-line", KeyMap.ctrl('U'));
        this.bind(emacs, "quoted-insert", KeyMap.ctrl('V'));
        this.bind(emacs, "backward-kill-word", KeyMap.ctrl('W'));
        this.bind(emacs, "yank", KeyMap.ctrl('Y'));
        this.bind(emacs, "character-search", KeyMap.ctrl(']'));
        this.bind(emacs, "undo", KeyMap.ctrl('_'));
        this.bind(emacs, "self-insert", KeyMap.range(" -~"));
        this.bind(emacs, "insert-close-paren", ")");
        this.bind(emacs, "insert-close-square", "]");
        this.bind(emacs, "insert-close-curly", "}");
        this.bind(emacs, "backward-delete-char", KeyMap.del());
        this.bind(emacs, "vi-match-bracket", KeyMap.translate("^X^B"));
        this.bind(emacs, "abort", KeyMap.translate("^X^G"));
        this.bind(emacs, "edit-and-execute-command", KeyMap.translate("^X^E"));
        this.bind(emacs, "vi-find-next-char", KeyMap.translate("^X^F"));
        this.bind(emacs, "vi-join", KeyMap.translate("^X^J"));
        this.bind(emacs, "kill-buffer", KeyMap.translate("^X^K"));
        this.bind(emacs, "infer-next-history", KeyMap.translate("^X^N"));
        this.bind(emacs, "overwrite-mode", KeyMap.translate("^X^O"));
        this.bind(emacs, "redo", KeyMap.translate("^X^R"));
        this.bind(emacs, "undo", KeyMap.translate("^X^U"));
        this.bind(emacs, "vi-cmd-mode", KeyMap.translate("^X^V"));
        this.bind(emacs, "exchange-point-and-mark", KeyMap.translate("^X^X"));
        this.bind(emacs, "do-lowercase-version", KeyMap.translate("^XA-^XZ"));
        this.bind(emacs, "what-cursor-position", KeyMap.translate("^X="));
        this.bind(emacs, "kill-line", KeyMap.translate("^X^?"));
        this.bind(emacs, "abort", KeyMap.alt(KeyMap.ctrl('G')));
        this.bind(emacs, "backward-kill-word", KeyMap.alt(KeyMap.ctrl('H')));
        this.bind(emacs, "self-insert-unmeta", KeyMap.alt(KeyMap.ctrl('M')));
        this.bind(emacs, "complete-word", KeyMap.alt(KeyMap.esc()));
        this.bind(emacs, "character-search-backward", KeyMap.alt(KeyMap.ctrl(']')));
        this.bind(emacs, "copy-prev-word", KeyMap.alt(KeyMap.ctrl('_')));
        this.bind(emacs, "set-mark-command", KeyMap.alt(' '));
        this.bind(emacs, "neg-argument", KeyMap.alt('-'));
        this.bind(emacs, "digit-argument", KeyMap.range("\\E0-\\E9"));
        this.bind(emacs, "beginning-of-history", KeyMap.alt('<'));
        this.bind(emacs, "list-choices", KeyMap.alt('='));
        this.bind(emacs, "end-of-history", KeyMap.alt('>'));
        this.bind(emacs, "list-choices", KeyMap.alt('?'));
        this.bind(emacs, "do-lowercase-version", KeyMap.range("^[A-^[Z"));
        this.bind(emacs, "accept-and-hold", KeyMap.alt('a'));
        this.bind(emacs, "backward-word", KeyMap.alt('b'));
        this.bind(emacs, "capitalize-word", KeyMap.alt('c'));
        this.bind(emacs, "kill-word", KeyMap.alt('d'));
        this.bind(emacs, "kill-word", KeyMap.translate("^[[3;5~"));
        this.bind(emacs, "forward-word", KeyMap.alt('f'));
        this.bind(emacs, "down-case-word", KeyMap.alt('l'));
        this.bind(emacs, "history-search-forward", KeyMap.alt('n'));
        this.bind(emacs, "history-search-backward", KeyMap.alt('p'));
        this.bind(emacs, "transpose-words", KeyMap.alt('t'));
        this.bind(emacs, "up-case-word", KeyMap.alt('u'));
        this.bind(emacs, "yank-pop", KeyMap.alt('y'));
        this.bind(emacs, "backward-kill-word", KeyMap.alt(KeyMap.del()));
        this.bindArrowKeys(emacs);
        this.bind(emacs, "forward-word", KeyMap.translate("^[[1;5C"));
        this.bind(emacs, "backward-word", KeyMap.translate("^[[1;5D"));
        this.bind(emacs, "forward-word", KeyMap.alt(this.key(InfoCmp.Capability.key_right)));
        this.bind(emacs, "backward-word", KeyMap.alt(this.key(InfoCmp.Capability.key_left)));
        this.bind(emacs, "forward-word", KeyMap.alt(KeyMap.translate("^[[C")));
        this.bind(emacs, "backward-word", KeyMap.alt(KeyMap.translate("^[[D")));
        return emacs;
    }
    
    public KeyMap<Binding> viInsertion() {
        final KeyMap<Binding> viins = new KeyMap<Binding>();
        this.bindKeys(viins);
        this.bind(viins, "self-insert", KeyMap.range("^@-^_"));
        this.bind(viins, "list-choices", KeyMap.ctrl('D'));
        this.bind(viins, "abort", KeyMap.ctrl('G'));
        this.bind(viins, "backward-delete-char", KeyMap.ctrl('H'));
        this.bind(viins, "expand-or-complete", KeyMap.ctrl('I'));
        this.bind(viins, "accept-line", KeyMap.ctrl('J'));
        this.bind(viins, "clear-screen", KeyMap.ctrl('L'));
        this.bind(viins, "accept-line", KeyMap.ctrl('M'));
        this.bind(viins, "menu-complete", KeyMap.ctrl('N'));
        this.bind(viins, "reverse-menu-complete", KeyMap.ctrl('P'));
        this.bind(viins, "history-incremental-search-backward", KeyMap.ctrl('R'));
        this.bind(viins, "history-incremental-search-forward", KeyMap.ctrl('S'));
        this.bind(viins, "transpose-chars", KeyMap.ctrl('T'));
        this.bind(viins, "kill-whole-line", KeyMap.ctrl('U'));
        this.bind(viins, "quoted-insert", KeyMap.ctrl('V'));
        this.bind(viins, "backward-kill-word", KeyMap.ctrl('W'));
        this.bind(viins, "yank", KeyMap.ctrl('Y'));
        this.bind(viins, "vi-cmd-mode", KeyMap.ctrl('['));
        this.bind(viins, "undo", KeyMap.ctrl('_'));
        this.bind(viins, "history-incremental-search-backward", KeyMap.ctrl('X') + "r");
        this.bind(viins, "history-incremental-search-forward", KeyMap.ctrl('X') + "s");
        this.bind(viins, "self-insert", KeyMap.range(" -~"));
        this.bind(viins, "insert-close-paren", ")");
        this.bind(viins, "insert-close-square", "]");
        this.bind(viins, "insert-close-curly", "}");
        this.bind(viins, "backward-delete-char", KeyMap.del());
        this.bindArrowKeys(viins);
        return viins;
    }
    
    public KeyMap<Binding> viCmd() {
        final KeyMap<Binding> vicmd = new KeyMap<Binding>();
        this.bind(vicmd, "list-choices", KeyMap.ctrl('D'));
        this.bind(vicmd, "emacs-editing-mode", KeyMap.ctrl('E'));
        this.bind(vicmd, "abort", KeyMap.ctrl('G'));
        this.bind(vicmd, "vi-backward-char", KeyMap.ctrl('H'));
        this.bind(vicmd, "accept-line", KeyMap.ctrl('J'));
        this.bind(vicmd, "kill-line", KeyMap.ctrl('K'));
        this.bind(vicmd, "clear-screen", KeyMap.ctrl('L'));
        this.bind(vicmd, "accept-line", KeyMap.ctrl('M'));
        this.bind(vicmd, "vi-down-line-or-history", KeyMap.ctrl('N'));
        this.bind(vicmd, "vi-up-line-or-history", KeyMap.ctrl('P'));
        this.bind(vicmd, "quoted-insert", KeyMap.ctrl('Q'));
        this.bind(vicmd, "history-incremental-search-backward", KeyMap.ctrl('R'));
        this.bind(vicmd, "history-incremental-search-forward", KeyMap.ctrl('S'));
        this.bind(vicmd, "transpose-chars", KeyMap.ctrl('T'));
        this.bind(vicmd, "kill-whole-line", KeyMap.ctrl('U'));
        this.bind(vicmd, "quoted-insert", KeyMap.ctrl('V'));
        this.bind(vicmd, "backward-kill-word", KeyMap.ctrl('W'));
        this.bind(vicmd, "yank", KeyMap.ctrl('Y'));
        this.bind(vicmd, "history-incremental-search-backward", KeyMap.ctrl('X') + "r");
        this.bind(vicmd, "history-incremental-search-forward", KeyMap.ctrl('X') + "s");
        this.bind(vicmd, "abort", KeyMap.alt(KeyMap.ctrl('G')));
        this.bind(vicmd, "backward-kill-word", KeyMap.alt(KeyMap.ctrl('H')));
        this.bind(vicmd, "self-insert-unmeta", KeyMap.alt(KeyMap.ctrl('M')));
        this.bind(vicmd, "complete-word", KeyMap.alt(KeyMap.esc()));
        this.bind(vicmd, "character-search-backward", KeyMap.alt(KeyMap.ctrl(']')));
        this.bind(vicmd, "set-mark-command", KeyMap.alt(' '));
        this.bind(vicmd, "digit-argument", KeyMap.alt('-'));
        this.bind(vicmd, "beginning-of-history", KeyMap.alt('<'));
        this.bind(vicmd, "list-choices", KeyMap.alt('='));
        this.bind(vicmd, "end-of-history", KeyMap.alt('>'));
        this.bind(vicmd, "list-choices", KeyMap.alt('?'));
        this.bind(vicmd, "do-lowercase-version", KeyMap.range("^[A-^[Z"));
        this.bind(vicmd, "backward-word", KeyMap.alt('b'));
        this.bind(vicmd, "capitalize-word", KeyMap.alt('c'));
        this.bind(vicmd, "kill-word", KeyMap.alt('d'));
        this.bind(vicmd, "forward-word", KeyMap.alt('f'));
        this.bind(vicmd, "down-case-word", KeyMap.alt('l'));
        this.bind(vicmd, "history-search-forward", KeyMap.alt('n'));
        this.bind(vicmd, "history-search-backward", KeyMap.alt('p'));
        this.bind(vicmd, "transpose-words", KeyMap.alt('t'));
        this.bind(vicmd, "up-case-word", KeyMap.alt('u'));
        this.bind(vicmd, "yank-pop", KeyMap.alt('y'));
        this.bind(vicmd, "backward-kill-word", KeyMap.alt(KeyMap.del()));
        this.bind(vicmd, "forward-char", " ");
        this.bind(vicmd, "vi-insert-comment", "#");
        this.bind(vicmd, "end-of-line", "$");
        this.bind(vicmd, "vi-match-bracket", "%");
        this.bind(vicmd, "vi-down-line-or-history", "+");
        this.bind(vicmd, "vi-rev-repeat-find", ",");
        this.bind(vicmd, "vi-up-line-or-history", "-");
        this.bind(vicmd, "vi-repeat-change", ".");
        this.bind(vicmd, "vi-history-search-backward", "/");
        this.bind(vicmd, "vi-digit-or-beginning-of-line", "0");
        this.bind(vicmd, "digit-argument", KeyMap.range("1-9"));
        this.bind(vicmd, "vi-repeat-find", ";");
        this.bind(vicmd, "list-choices", "=");
        this.bind(vicmd, "vi-history-search-forward", "?");
        this.bind(vicmd, "vi-add-eol", "A");
        this.bind(vicmd, "vi-backward-blank-word", "B");
        this.bind(vicmd, "vi-change-eol", "C");
        this.bind(vicmd, "vi-kill-eol", "D");
        this.bind(vicmd, "vi-forward-blank-word-end", "E");
        this.bind(vicmd, "vi-find-prev-char", "F");
        this.bind(vicmd, "vi-fetch-history", "G");
        this.bind(vicmd, "vi-insert-bol", "I");
        this.bind(vicmd, "vi-join", "J");
        this.bind(vicmd, "vi-rev-repeat-search", "N");
        this.bind(vicmd, "vi-open-line-above", "O");
        this.bind(vicmd, "vi-put-before", "P");
        this.bind(vicmd, "vi-replace", "R");
        this.bind(vicmd, "vi-kill-line", "S");
        this.bind(vicmd, "vi-find-prev-char-skip", "T");
        this.bind(vicmd, "redo", "U");
        this.bind(vicmd, "visual-line-mode", "V");
        this.bind(vicmd, "vi-forward-blank-word", "W");
        this.bind(vicmd, "vi-backward-delete-char", "X");
        this.bind(vicmd, "vi-yank-whole-line", "Y");
        this.bind(vicmd, "vi-first-non-blank", "^");
        this.bind(vicmd, "vi-add-next", "a");
        this.bind(vicmd, "vi-backward-word", "b");
        this.bind(vicmd, "vi-change-to", "c");
        this.bind(vicmd, "vi-delete", "d");
        this.bind(vicmd, "vi-forward-word-end", "e");
        this.bind(vicmd, "vi-find-next-char", "f");
        this.bind(vicmd, "what-cursor-position", "ga");
        this.bind(vicmd, "vi-backward-blank-word-end", "gE");
        this.bind(vicmd, "vi-backward-word-end", "ge");
        this.bind(vicmd, "vi-backward-char", "h");
        this.bind(vicmd, "vi-insert", "i");
        this.bind(vicmd, "down-line-or-history", "j");
        this.bind(vicmd, "up-line-or-history", "k");
        this.bind(vicmd, "vi-forward-char", "l");
        this.bind(vicmd, "vi-repeat-search", "n");
        this.bind(vicmd, "vi-open-line-below", "o");
        this.bind(vicmd, "vi-put-after", "p");
        this.bind(vicmd, "vi-replace-chars", "r");
        this.bind(vicmd, "vi-substitute", "s");
        this.bind(vicmd, "vi-find-next-char-skip", "t");
        this.bind(vicmd, "undo", "u");
        this.bind(vicmd, "visual-mode", "v");
        this.bind(vicmd, "vi-forward-word", "w");
        this.bind(vicmd, "vi-delete-char", "x");
        this.bind(vicmd, "vi-yank", "y");
        this.bind(vicmd, "vi-goto-column", "|");
        this.bind(vicmd, "vi-swap-case", "~");
        this.bind(vicmd, "vi-backward-char", KeyMap.del());
        this.bindArrowKeys(vicmd);
        return vicmd;
    }
    
    public KeyMap<Binding> menu() {
        final KeyMap<Binding> menu = new KeyMap<Binding>();
        this.bind(menu, "menu-complete", "\t");
        this.bind(menu, "reverse-menu-complete", this.key(InfoCmp.Capability.back_tab));
        this.bind(menu, "accept-line", "\r", "\n");
        this.bindArrowKeys(menu);
        return menu;
    }
    
    public KeyMap<Binding> safe() {
        final KeyMap<Binding> safe = new KeyMap<Binding>();
        this.bind(safe, "self-insert", KeyMap.range("^@-^?"));
        this.bind(safe, "accept-line", "\r", "\n");
        this.bind(safe, "abort", KeyMap.ctrl('G'));
        return safe;
    }
    
    public KeyMap<Binding> dumb() {
        final KeyMap<Binding> dumb = new KeyMap<Binding>();
        this.bind(dumb, "self-insert", KeyMap.range("^@-^?"));
        this.bind(dumb, "accept-line", "\r", "\n");
        this.bind(dumb, "beep", KeyMap.ctrl('G'));
        return dumb;
    }
    
    public KeyMap<Binding> visual() {
        final KeyMap<Binding> visual = new KeyMap<Binding>();
        this.bind(visual, "up-line", this.key(InfoCmp.Capability.key_up), "k");
        this.bind(visual, "down-line", this.key(InfoCmp.Capability.key_down), "j");
        this.bind(visual, this::deactivateRegion, KeyMap.esc());
        this.bind(visual, "exchange-point-and-mark", "o");
        this.bind(visual, "put-replace-selection", "p");
        this.bind(visual, "vi-delete", "x");
        this.bind(visual, "vi-oper-swap-case", "~");
        return visual;
    }
    
    public KeyMap<Binding> viOpp() {
        final KeyMap<Binding> viOpp = new KeyMap<Binding>();
        this.bind(viOpp, "up-line", this.key(InfoCmp.Capability.key_up), "k");
        this.bind(viOpp, "down-line", this.key(InfoCmp.Capability.key_down), "j");
        this.bind(viOpp, "vi-cmd-mode", KeyMap.esc());
        return viOpp;
    }
    
    private void bind(final KeyMap<Binding> map, final String widget, final Iterable<? extends CharSequence> keySeqs) {
        map.bind(new Reference(widget), keySeqs);
    }
    
    private void bind(final KeyMap<Binding> map, final String widget, final CharSequence... keySeqs) {
        map.bind(new Reference(widget), keySeqs);
    }
    
    private void bind(final KeyMap<Binding> map, final Widget widget, final CharSequence... keySeqs) {
        map.bind(widget, keySeqs);
    }
    
    private String key(final InfoCmp.Capability capability) {
        return KeyMap.key(this.terminal, capability);
    }
    
    private void bindKeys(final KeyMap<Binding> emacs) {
        final Widget beep = this.namedWidget("beep", this::beep);
        Stream.of(InfoCmp.Capability.values()).filter(c -> c.name().startsWith("key_")).map((Function<? super InfoCmp.Capability, ?>)this::key).forEach(k -> this.bind(emacs, beep, k));
    }
    
    private void bindArrowKeys(final KeyMap<Binding> map) {
        this.bind(map, "up-line-or-search", this.key(InfoCmp.Capability.key_up));
        this.bind(map, "down-line-or-search", this.key(InfoCmp.Capability.key_down));
        this.bind(map, "backward-char", this.key(InfoCmp.Capability.key_left));
        this.bind(map, "forward-char", this.key(InfoCmp.Capability.key_right));
        this.bind(map, "beginning-of-line", this.key(InfoCmp.Capability.key_home));
        this.bind(map, "end-of-line", this.key(InfoCmp.Capability.key_end));
        this.bind(map, "delete-char", this.key(InfoCmp.Capability.key_dc));
        this.bind(map, "kill-whole-line", this.key(InfoCmp.Capability.key_dl));
        this.bind(map, "overwrite-mode", this.key(InfoCmp.Capability.key_ic));
        this.bind(map, "mouse", (CharSequence[])MouseSupport.keys(this.terminal));
        this.bind(map, "begin-paste", "\u001b[200~");
        this.bind(map, "terminal-focus-in", "\u001b[I");
        this.bind(map, "terminal-focus-out", "\u001b[O");
    }
    
    private void bindConsoleChars(final KeyMap<Binding> keyMap, final Attributes attr) {
        if (attr != null) {
            this.rebind(keyMap, "backward-delete-char", KeyMap.del(), (char)attr.getControlChar(Attributes.ControlChar.VERASE));
            this.rebind(keyMap, "backward-kill-word", KeyMap.ctrl('W'), (char)attr.getControlChar(Attributes.ControlChar.VWERASE));
            this.rebind(keyMap, "kill-whole-line", KeyMap.ctrl('U'), (char)attr.getControlChar(Attributes.ControlChar.VKILL));
            this.rebind(keyMap, "quoted-insert", KeyMap.ctrl('V'), (char)attr.getControlChar(Attributes.ControlChar.VLNEXT));
        }
    }
    
    private void rebind(final KeyMap<Binding> keyMap, final String operation, final String prevBinding, final char newBinding) {
        if (newBinding > '\0' && newBinding < '\u0080') {
            final Reference ref = new Reference(operation);
            this.bind(keyMap, "self-insert", prevBinding);
            keyMap.bind(ref, Character.toString(newBinding));
        }
    }
    
    @Override
    public void zeroOut() {
        this.buf.zeroOut();
        this.parsedLine = null;
    }
    
    protected enum State
    {
        NORMAL, 
        DONE, 
        IGNORE, 
        EOF, 
        INTERRUPT;
    }
    
    protected enum ViMoveMode
    {
        NORMAL, 
        YANK, 
        DELETE, 
        CHANGE;
    }
    
    protected enum BellType
    {
        NONE, 
        AUDIBLE, 
        VISIBLE;
    }
    
    static class Pair<U, V>
    {
        final U u;
        final V v;
        
        public Pair(final U u, final V v) {
            this.u = u;
            this.v = v;
        }
        
        public U getU() {
            return this.u;
        }
        
        public V getV() {
            return this.v;
        }
    }
    
    protected enum CompletionType
    {
        Expand, 
        ExpandComplete, 
        Complete, 
        List;
    }
    
    private class MenuSupport implements Supplier<AttributedString>
    {
        final List<Candidate> possible;
        final BiFunction<CharSequence, Boolean, CharSequence> escaper;
        int selection;
        int topLine;
        String word;
        AttributedString computed;
        int lines;
        int columns;
        String completed;
        
        public MenuSupport(final List<Candidate> original, final String completed, final BiFunction<CharSequence, Boolean, CharSequence> escaper) {
            this.possible = new ArrayList<Candidate>();
            this.escaper = escaper;
            this.selection = -1;
            this.topLine = 0;
            this.word = "";
            this.completed = completed;
            LineReaderImpl.this.computePost(original, null, this.possible, completed);
            this.next();
        }
        
        public Candidate completion() {
            return this.possible.get(this.selection);
        }
        
        public void next() {
            this.selection = (this.selection + 1) % this.possible.size();
            this.update();
        }
        
        public void previous() {
            this.selection = (this.selection + this.possible.size() - 1) % this.possible.size();
            this.update();
        }
        
        private void major(final int step) {
            final int axis = LineReaderImpl.this.isSet(Option.LIST_ROWS_FIRST) ? this.columns : this.lines;
            int sel = this.selection + step * axis;
            if (sel < 0) {
                final int pos = (sel + axis) % axis;
                final int remainders = this.possible.size() % axis;
                sel = this.possible.size() - remainders + pos;
                if (sel >= this.possible.size()) {
                    sel -= axis;
                }
            }
            else if (sel >= this.possible.size()) {
                sel %= axis;
            }
            this.selection = sel;
            this.update();
        }
        
        private void minor(final int step) {
            int axis = LineReaderImpl.this.isSet(Option.LIST_ROWS_FIRST) ? this.columns : this.lines;
            final int row = this.selection % axis;
            final int options = this.possible.size();
            if (this.selection - row + axis > options) {
                axis = options % axis;
            }
            this.selection = this.selection - row + (axis + row + step) % axis;
            this.update();
        }
        
        public void up() {
            if (LineReaderImpl.this.isSet(Option.LIST_ROWS_FIRST)) {
                this.major(-1);
            }
            else {
                this.minor(-1);
            }
        }
        
        public void down() {
            if (LineReaderImpl.this.isSet(Option.LIST_ROWS_FIRST)) {
                this.major(1);
            }
            else {
                this.minor(1);
            }
        }
        
        public void left() {
            if (LineReaderImpl.this.isSet(Option.LIST_ROWS_FIRST)) {
                this.minor(-1);
            }
            else {
                this.major(-1);
            }
        }
        
        public void right() {
            if (LineReaderImpl.this.isSet(Option.LIST_ROWS_FIRST)) {
                this.minor(1);
            }
            else {
                this.major(1);
            }
        }
        
        private void update() {
            LineReaderImpl.this.buf.backspace(this.word.length());
            this.word = this.escaper.apply(this.completion().value(), true).toString();
            LineReaderImpl.this.buf.write(this.word);
            final PostResult pr = LineReaderImpl.this.computePost(this.possible, this.completion(), null, this.completed);
            final int displaySize = LineReaderImpl.this.displayRows() - LineReaderImpl.this.promptLines();
            if (pr.lines > displaySize) {
                final int displayed = displaySize - 1;
                if (pr.selectedLine >= 0) {
                    if (pr.selectedLine < this.topLine) {
                        this.topLine = pr.selectedLine;
                    }
                    else if (pr.selectedLine >= this.topLine + displayed) {
                        this.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();
                }
                final List<AttributedString> lines = post.columnSplitLength(LineReaderImpl.this.size.getColumns(), true, LineReaderImpl.this.display.delayLineWrap());
                final List<AttributedString> sub = new ArrayList<AttributedString>(lines.subList(this.topLine, this.topLine + displayed));
                sub.add(new AttributedStringBuilder().style(AttributedStyle.DEFAULT.foreground(6)).append("rows ").append(Integer.toString(this.topLine + 1)).append(" to ").append(Integer.toString(this.topLine + displayed)).append(" of ").append(Integer.toString(lines.size())).append("\n").style(AttributedStyle.DEFAULT).toAttributedString());
                this.computed = AttributedString.join(AttributedString.EMPTY, sub);
            }
            else {
                this.computed = pr.post;
            }
            this.lines = pr.lines;
            this.columns = (this.possible.size() + this.lines - 1) / this.lines;
        }
        
        @Override
        public AttributedString get() {
            return this.computed;
        }
    }
    
    private static class CompletingWord implements CompletingParsedLine
    {
        private final String word;
        
        public CompletingWord(final String word) {
            this.word = word;
        }
        
        @Override
        public CharSequence escape(final CharSequence candidate, final boolean complete) {
            return null;
        }
        
        @Override
        public int rawWordCursor() {
            return this.word.length();
        }
        
        @Override
        public int rawWordLength() {
            return this.word.length();
        }
        
        @Override
        public String word() {
            return this.word;
        }
        
        @Override
        public int wordCursor() {
            return this.word.length();
        }
        
        @Override
        public int wordIndex() {
            return 0;
        }
        
        @Override
        public List<String> words() {
            return null;
        }
        
        @Override
        public String line() {
            return this.word;
        }
        
        @Override
        public int cursor() {
            return this.word.length();
        }
    }
    
    protected static class PostResult
    {
        final AttributedString post;
        final int lines;
        final int selectedLine;
        
        public PostResult(final AttributedString post, final int lines, final int selectedLine) {
            this.post = post;
            this.lines = lines;
            this.selectedLine = selectedLine;
        }
    }
    
    private static class TerminalLine
    {
        private String endLine;
        private int startPos;
        
        public TerminalLine(final String line, final int startPos, final int width) {
            this.startPos = startPos;
            this.endLine = line.substring(line.lastIndexOf(10) + 1);
            boolean first;
            for (first = true; this.endLine.length() + (first ? startPos : 0) > width && width > 0; first = false) {
                if (first) {
                    this.endLine = this.endLine.substring(width - startPos);
                }
                else {
                    this.endLine = this.endLine.substring(width);
                }
            }
            if (!first) {
                this.startPos = 0;
            }
        }
        
        public int getStartPos() {
            return this.startPos;
        }
        
        public String getEndLine() {
            return this.endLine;
        }
    }
}
