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

package org.jline.builtins;

import java.io.InterruptedIOException;
import java.io.FilterInputStream;
import java.util.regex.Pattern;
import org.jline.utils.AttributedStyle;
import org.jline.utils.AttributedStringBuilder;
import java.io.Reader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.regex.PatternSyntaxException;
import java.util.Iterator;
import org.jline.terminal.Attributes;
import java.io.FileNotFoundException;
import java.util.HashMap;
import org.jline.utils.InfoCmp;
import org.jline.utils.Status;
import java.util.Collection;
import java.util.Arrays;
import java.util.stream.Stream;
import java.nio.file.PathMatcher;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.Objects;
import java.nio.file.Files;
import java.nio.file.FileVisitOption;
import java.nio.file.Paths;
import java.nio.file.FileSystems;
import java.io.File;
import java.io.IOException;
import java.util.TreeMap;
import java.util.ArrayList;
import java.util.Collections;
import org.jline.terminal.Size;
import java.util.Map;
import org.jline.utils.AttributedString;
import org.jline.keymap.KeyMap;
import java.io.BufferedReader;
import java.nio.file.Path;
import org.jline.keymap.BindingReader;
import org.jline.utils.Display;
import org.jline.terminal.Terminal;
import java.util.List;

public class Less
{
    private static final int ESCAPE = 27;
    private static final String MESSAGE_FILE_INFO = "FILE_INFO";
    public boolean quitAtSecondEof;
    public boolean quitAtFirstEof;
    public boolean quitIfOneScreen;
    public boolean printLineNumbers;
    public boolean quiet;
    public boolean veryQuiet;
    public boolean chopLongLines;
    public boolean ignoreCaseCond;
    public boolean ignoreCaseAlways;
    public boolean noKeypad;
    public boolean noInit;
    protected List<Integer> tabs;
    protected String syntaxName;
    private String historyLog;
    protected final Terminal terminal;
    protected final Display display;
    protected final BindingReader bindingReader;
    protected final Path currentDir;
    protected List<Source> sources;
    protected int sourceIdx;
    protected BufferedReader reader;
    protected KeyMap<Operation> keys;
    protected int firstLineInMemory;
    protected List<AttributedString> lines;
    protected int firstLineToDisplay;
    protected int firstColumnToDisplay;
    protected int offsetInLine;
    protected String message;
    protected String errorMessage;
    protected final StringBuilder buffer;
    protected final Map<String, Operation> options;
    protected int window;
    protected int halfWindow;
    protected int nbEof;
    protected Nano.PatternHistory patternHistory;
    protected String pattern;
    protected String displayPattern;
    protected final Size size;
    SyntaxHighlighter syntaxHighlighter;
    private final List<Path> syntaxFiles;
    private boolean highlight;
    private boolean nanorcIgnoreErrors;
    
    public static String[] usage() {
        return new String[] { "less -  file pager", "Usage: less [OPTIONS] [FILES]", "  -? --help                    Show help", "  -e --quit-at-eof             Exit on second EOF", "  -E --QUIT-AT-EOF             Exit on EOF", "  -F --quit-if-one-screen      Exit if entire file fits on first screen", "  -q --quiet --silent          Silent mode", "  -Q --QUIET --SILENT          Completely silent", "  -S --chop-long-lines         Do not fold long lines", "  -i --ignore-case             Search ignores lowercase case", "  -I --IGNORE-CASE             Search ignores all case", "  -x --tabs=N[,...]            Set tab stops", "  -N --LINE-NUMBERS            Display line number for each line", "  -Y --syntax=name             The name of the syntax highlighting to use.", "     --no-init                 Disable terminal initialization", "     --no-keypad               Disable keypad handling", "     --ignorercfiles           Don't look at the system's lessrc nor at the user's lessrc.", "  -H --historylog=name         Log search strings to file, so they can be retrieved in later sessions" };
    }
    
    public Less(final Terminal terminal, final Path currentDir) {
        this(terminal, currentDir, null);
    }
    
    public Less(final Terminal terminal, final Path currentDir, final Options opts) {
        this(terminal, currentDir, opts, null);
    }
    
    public Less(final Terminal terminal, final Path currentDir, final Options opts, final ConfigurationPath configPath) {
        this.tabs = Collections.singletonList(4);
        this.historyLog = null;
        this.firstLineInMemory = 0;
        this.lines = new ArrayList<AttributedString>();
        this.firstLineToDisplay = 0;
        this.firstColumnToDisplay = 0;
        this.offsetInLine = 0;
        this.buffer = new StringBuilder();
        this.options = new TreeMap<String, Operation>();
        this.patternHistory = new Nano.PatternHistory(null);
        this.size = new Size();
        this.syntaxFiles = new ArrayList<Path>();
        this.highlight = true;
        this.terminal = terminal;
        this.display = new Display(terminal, true);
        this.bindingReader = new BindingReader(terminal.reader());
        this.currentDir = currentDir;
        final Path lessrc = (configPath != null) ? configPath.getConfig("jlessrc") : null;
        final boolean ignorercfiles = opts != null && opts.isSet("ignorercfiles");
        if (lessrc != null && !ignorercfiles) {
            try {
                this.parseConfig(lessrc);
            }
            catch (final IOException e) {
                this.errorMessage = "Encountered error while reading config file: " + lessrc;
            }
        }
        else if (new File("/usr/share/nano").exists() && !ignorercfiles) {
            final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:/usr/share/nano/*.nanorc");
            try (final Stream<Path> pathStream = Files.walk(Paths.get("/usr/share/nano", new String[0]), new FileVisitOption[0])) {
                final Stream<Path> stream = pathStream;
                final PathMatcher obj = pathMatcher;
                Objects.requireNonNull(obj);
                final Stream<Path> filter = stream.filter(obj::matches);
                final List<Path> syntaxFiles = this.syntaxFiles;
                Objects.requireNonNull(syntaxFiles);
                filter.forEach(syntaxFiles::add);
                this.nanorcIgnoreErrors = true;
            }
            catch (final IOException e2) {
                this.errorMessage = "Encountered error while reading nanorc files";
            }
        }
        if (opts != null) {
            if (opts.isSet("QUIT-AT-EOF")) {
                this.quitAtFirstEof = true;
            }
            if (opts.isSet("quit-at-eof")) {
                this.quitAtSecondEof = true;
            }
            if (opts.isSet("quit-if-one-screen")) {
                this.quitIfOneScreen = true;
            }
            if (opts.isSet("quiet")) {
                this.quiet = true;
            }
            if (opts.isSet("QUIET")) {
                this.veryQuiet = true;
            }
            if (opts.isSet("chop-long-lines")) {
                this.chopLongLines = true;
            }
            if (opts.isSet("IGNORE-CASE")) {
                this.ignoreCaseAlways = true;
            }
            if (opts.isSet("ignore-case")) {
                this.ignoreCaseCond = true;
            }
            if (opts.isSet("LINE-NUMBERS")) {
                this.printLineNumbers = true;
            }
            if (opts.isSet("tabs")) {
                this.doTabs(opts.get("tabs"));
            }
            if (opts.isSet("syntax")) {
                this.syntaxName = opts.get("syntax");
                this.nanorcIgnoreErrors = false;
            }
            if (opts.isSet("no-init")) {
                this.noInit = true;
            }
            if (opts.isSet("no-keypad")) {
                this.noKeypad = true;
            }
            if (opts.isSet("historylog")) {
                this.historyLog = opts.get("historylog");
            }
        }
        if (configPath != null && this.historyLog != null) {
            try {
                this.patternHistory = new Nano.PatternHistory(configPath.getUserConfig(this.historyLog, true));
            }
            catch (final IOException e) {
                this.errorMessage = "Encountered error while reading pattern-history file: " + this.historyLog;
            }
        }
    }
    
    private void parseConfig(final Path file) throws IOException {
        try (final BufferedReader reader = Files.newBufferedReader(file)) {
            for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                line = line.trim();
                if (!line.isEmpty() && !line.startsWith("#")) {
                    final List<String> parts = SyntaxHighlighter.RuleSplitter.split(line);
                    if (parts.get(0).equals("include")) {
                        SyntaxHighlighter.nanorcInclude(file, parts.get(1), this.syntaxFiles);
                    }
                    else if (parts.get(0).equals("theme")) {
                        SyntaxHighlighter.nanorcTheme(file, parts.get(1), this.syntaxFiles);
                    }
                    else if (parts.size() == 2 && (parts.get(0).equals("set") || parts.get(0).equals("unset"))) {
                        final String option = parts.get(1);
                        final boolean val = parts.get(0).equals("set");
                        if (option.equals("QUIT-AT-EOF")) {
                            this.quitAtFirstEof = val;
                        }
                        else if (option.equals("quit-at-eof")) {
                            this.quitAtSecondEof = val;
                        }
                        else if (option.equals("quit-if-one-screen")) {
                            this.quitIfOneScreen = val;
                        }
                        else if (option.equals("quiet") || option.equals("silent")) {
                            this.quiet = val;
                        }
                        else if (option.equals("QUIET") || option.equals("SILENT")) {
                            this.veryQuiet = val;
                        }
                        else if (option.equals("chop-long-lines")) {
                            this.chopLongLines = val;
                        }
                        else if (option.equals("IGNORE-CASE")) {
                            this.ignoreCaseAlways = val;
                        }
                        else if (option.equals("ignore-case")) {
                            this.ignoreCaseCond = val;
                        }
                        else if (option.equals("LINE-NUMBERS")) {
                            this.printLineNumbers = val;
                        }
                        else {
                            this.errorMessage = "Less config: Unknown or unsupported configuration option " + option;
                        }
                    }
                    else if (parts.size() == 3 && parts.get(0).equals("set")) {
                        final String option = parts.get(1);
                        final String val2 = parts.get(2);
                        if (option.equals("tabs")) {
                            this.doTabs(val2);
                        }
                        else if (option.equals("historylog")) {
                            this.historyLog = val2;
                        }
                        else {
                            this.errorMessage = "Less config: Unknown or unsupported configuration option " + option;
                        }
                    }
                    else if (parts.get(0).equals("bind") || parts.get(0).equals("unbind")) {
                        this.errorMessage = "Less config: Key bindings can not be changed!";
                    }
                    else {
                        this.errorMessage = "Less config: Bad configuration '" + line + "'";
                    }
                }
            }
        }
    }
    
    private void doTabs(final String val) {
        this.tabs = new ArrayList<Integer>();
        for (final String s : val.split(",")) {
            try {
                this.tabs.add(Integer.parseInt(s));
            }
            catch (final Exception ex) {
                this.errorMessage = "Less config: tabs option error parsing number: " + s;
            }
        }
    }
    
    public Less tabs(final List<Integer> tabs) {
        this.tabs = tabs;
        return this;
    }
    
    public void handle(final Terminal.Signal signal) {
        this.size.copy(this.terminal.getSize());
        try {
            this.display.clear();
            this.display(false);
        }
        catch (final IOException e) {
            e.printStackTrace();
        }
    }
    
    public void run(final Source... sources) throws IOException, InterruptedException {
        this.run(new ArrayList<Source>(Arrays.asList(sources)));
    }
    
    public void run(final List<Source> sources) throws IOException, InterruptedException {
        if (sources == null || sources.isEmpty()) {
            throw new IllegalArgumentException("No sources");
        }
        sources.add(0, new Source.ResourceSource("less-help.txt", "HELP -- Press SPACE for more, or q when done"));
        this.sources = sources;
        this.sourceIdx = 1;
        this.openSource();
        if (this.errorMessage != null) {
            this.message = this.errorMessage;
            this.errorMessage = null;
        }
        final Status status = Status.getStatus(this.terminal, false);
        try {
            if (status != null) {
                status.suspend();
            }
            this.size.copy(this.terminal.getSize());
            if (this.quitIfOneScreen && sources.size() == 2 && this.display(true)) {
                return;
            }
            final Terminal.SignalHandler prevHandler = this.terminal.handle(Terminal.Signal.WINCH, this::handle);
            final Attributes attr = this.terminal.enterRawMode();
            try {
                this.window = this.size.getRows() - 1;
                this.halfWindow = this.window / 2;
                this.bindKeys(this.keys = new KeyMap<Operation>());
                if (!this.noInit) {
                    this.terminal.puts(InfoCmp.Capability.enter_ca_mode, new Object[0]);
                }
                if (!this.noKeypad) {
                    this.terminal.puts(InfoCmp.Capability.keypad_xmit, new Object[0]);
                }
                this.terminal.writer().flush();
                this.display.clear();
                this.display(false);
                checkInterrupted();
                this.options.put("-e", Operation.OPT_QUIT_AT_SECOND_EOF);
                this.options.put("--quit-at-eof", Operation.OPT_QUIT_AT_SECOND_EOF);
                this.options.put("-E", Operation.OPT_QUIT_AT_FIRST_EOF);
                this.options.put("-QUIT-AT-EOF", Operation.OPT_QUIT_AT_FIRST_EOF);
                this.options.put("-N", Operation.OPT_PRINT_LINES);
                this.options.put("--LINE-NUMBERS", Operation.OPT_PRINT_LINES);
                this.options.put("-q", Operation.OPT_QUIET);
                this.options.put("--quiet", Operation.OPT_QUIET);
                this.options.put("--silent", Operation.OPT_QUIET);
                this.options.put("-Q", Operation.OPT_VERY_QUIET);
                this.options.put("--QUIET", Operation.OPT_VERY_QUIET);
                this.options.put("--SILENT", Operation.OPT_VERY_QUIET);
                this.options.put("-S", Operation.OPT_CHOP_LONG_LINES);
                this.options.put("--chop-long-lines", Operation.OPT_CHOP_LONG_LINES);
                this.options.put("-i", Operation.OPT_IGNORE_CASE_COND);
                this.options.put("--ignore-case", Operation.OPT_IGNORE_CASE_COND);
                this.options.put("-I", Operation.OPT_IGNORE_CASE_ALWAYS);
                this.options.put("--IGNORE-CASE", Operation.OPT_IGNORE_CASE_ALWAYS);
                this.options.put("-Y", Operation.OPT_SYNTAX_HIGHLIGHT);
                this.options.put("--syntax", Operation.OPT_SYNTAX_HIGHLIGHT);
                boolean forward = true;
                Operation op;
                do {
                    checkInterrupted();
                    op = null;
                    if (this.buffer.length() > 0 && this.buffer.charAt() == '-') {
                        final int c = this.terminal.reader().read();
                        this.message = null;
                        if (this.buffer.length() == 1) {
                            this.buffer.append((char)c);
                            if (c != 45) {
                                op = this.options.get(this.buffer.toString());
                                if (op == null) {
                                    this.message = "There is no " + this.printable(this.buffer.toString()) + " option";
                                    this.buffer.setLength();
                                }
                            }
                        }
                        else if (c == 13) {
                            op = this.options.get(this.buffer.toString());
                            if (op == null) {
                                this.message = "There is no " + this.printable(this.buffer.toString()) + " option";
                                this.buffer.setLength();
                            }
                        }
                        else {
                            this.buffer.append((char)c);
                            final Map<String, Operation> matching = new HashMap<String, Operation>();
                            for (final Map.Entry<String, Operation> entry : this.options.entrySet()) {
                                if (entry.getKey().startsWith(this.buffer.toString())) {
                                    matching.put(entry.getKey(), entry.getValue());
                                }
                            }
                            switch (matching.size()) {
                                case 0: {
                                    this.buffer.setLength();
                                    break;
                                }
                                case 1: {
                                    this.buffer.setLength();
                                    this.buffer.append(matching.keySet().iterator().next());
                                    break;
                                }
                            }
                        }
                    }
                    else if (this.buffer.length() > 0 && (this.buffer.charAt() == '/' || this.buffer.charAt() == '?' || this.buffer.charAt() == '&')) {
                        forward = this.search();
                    }
                    else {
                        final Operation obj = this.bindingReader.readBinding(this.keys, null, false);
                        if (obj == Operation.CHAR) {
                            final char c2 = this.bindingReader.getLastBinding().charAt(0);
                            if (c2 == '-' || c2 == '/' || c2 == '?' || c2 == '&') {
                                this.buffer.setLength();
                            }
                            this.buffer.append(c2);
                        }
                        else if (obj == Operation.BACKSPACE) {
                            if (this.buffer.length() > 0) {
                                this.buffer.deleteCharAt(this.buffer.length() - 1);
                            }
                        }
                        else {
                            op = obj;
                        }
                    }
                    if (op != null) {
                        this.message = null;
                        switch (op.ordinal()) {
                            case 2: {
                                this.moveForward(this.getStrictPositiveNumberInBuffer(1));
                                break;
                            }
                            case 3: {
                                this.moveBackward(this.getStrictPositiveNumberInBuffer(1));
                                break;
                            }
                            case 4: {
                                this.moveForward(this.getStrictPositiveNumberInBuffer(this.window));
                                break;
                            }
                            case 6: {
                                this.moveForward(this.window = this.getStrictPositiveNumberInBuffer(this.window));
                                break;
                            }
                            case 8: {
                                this.moveForward(this.window);
                                break;
                            }
                            case 9: {
                                this.moveForward(this.halfWindow = this.getStrictPositiveNumberInBuffer(this.halfWindow));
                                break;
                            }
                            case 7: {
                                this.moveBackward(this.window = this.getStrictPositiveNumberInBuffer(this.window));
                                break;
                            }
                            case 5: {
                                this.moveBackward(this.getStrictPositiveNumberInBuffer(this.window));
                                break;
                            }
                            case 10: {
                                this.moveBackward(this.halfWindow = this.getStrictPositiveNumberInBuffer(this.halfWindow));
                                break;
                            }
                            case 21: {
                                this.moveTo(this.getStrictPositiveNumberInBuffer(1) - 1);
                                break;
                            }
                            case 22: {
                                final int lineNum = this.getStrictPositiveNumberInBuffer(0) - 1;
                                if (lineNum < 0) {
                                    this.moveForward(Integer.MAX_VALUE);
                                    break;
                                }
                                this.moveTo(lineNum);
                                break;
                            }
                            case 49: {
                                this.moveTo(0);
                                break;
                            }
                            case 50: {
                                this.moveForward(Integer.MAX_VALUE);
                                break;
                            }
                            case 11: {
                                this.firstColumnToDisplay = Math.max(0, this.firstColumnToDisplay - this.size.getColumns() / 2);
                                break;
                            }
                            case 12: {
                                this.firstColumnToDisplay += this.size.getColumns() / 2;
                                break;
                            }
                            case 19: {
                                this.moveToMatch(!forward, true);
                                break;
                            }
                            case 17: {
                                this.moveToMatch(!forward, false);
                                break;
                            }
                            case 18: {
                                this.moveToMatch(forward, true);
                                break;
                            }
                            case 16: {
                                this.moveToMatch(forward, false);
                                break;
                            }
                            case 20: {
                                this.pattern = null;
                                break;
                            }
                            case 28: {
                                this.buffer.setLength();
                                this.printLineNumbers = !this.printLineNumbers;
                                this.message = (this.printLineNumbers ? "Constantly display line numbers" : "Don't use line numbers");
                                break;
                            }
                            case 32: {
                                this.buffer.setLength();
                                this.quiet = !this.quiet;
                                this.veryQuiet = false;
                                this.message = (this.quiet ? "Ring the bell for errors but not at eof/bof" : "Ring the bell for errors AND at eof/bof");
                                break;
                            }
                            case 33: {
                                this.buffer.setLength();
                                this.veryQuiet = !this.veryQuiet;
                                this.quiet = false;
                                this.message = (this.veryQuiet ? "Never ring the bell" : "Ring the bell for errors AND at eof/bof");
                                break;
                            }
                            case 29: {
                                this.buffer.setLength();
                                this.offsetInLine = 0;
                                this.chopLongLines = !this.chopLongLines;
                                this.message = (this.chopLongLines ? "Chop long lines" : "Fold long lines");
                                this.display.clear();
                                break;
                            }
                            case 34: {
                                this.ignoreCaseCond = !this.ignoreCaseCond;
                                this.ignoreCaseAlways = false;
                                this.message = (this.ignoreCaseCond ? "Ignore case in searches" : "Case is significant in searches");
                                break;
                            }
                            case 35: {
                                this.ignoreCaseAlways = !this.ignoreCaseAlways;
                                this.ignoreCaseCond = false;
                                this.message = (this.ignoreCaseAlways ? "Ignore case in searches and in patterns" : "Case is significant in searches");
                                break;
                            }
                            case 36: {
                                this.highlight = !this.highlight;
                                this.message = "Highlight " + (this.highlight ? "enabled" : "disabled");
                                break;
                            }
                            case 37: {
                                this.addFile();
                                break;
                            }
                            case 38: {
                                final int next = this.getStrictPositiveNumberInBuffer(1);
                                if (this.sourceIdx < sources.size() - next) {
                                    final SavedSourcePositions ssp = new SavedSourcePositions();
                                    this.sourceIdx += next;
                                    final String newSource = sources.get(this.sourceIdx).getName();
                                    try {
                                        this.openSource();
                                    }
                                    catch (final FileNotFoundException exp) {
                                        ssp.restore(newSource);
                                    }
                                    break;
                                }
                                this.message = "No next file";
                                break;
                            }
                            case 39: {
                                final int prev = this.getStrictPositiveNumberInBuffer(1);
                                if (this.sourceIdx > prev) {
                                    final SavedSourcePositions ssp2 = new SavedSourcePositions(-1);
                                    this.sourceIdx -= prev;
                                    final String newSource2 = sources.get(this.sourceIdx).getName();
                                    try {
                                        this.openSource();
                                    }
                                    catch (final FileNotFoundException exp2) {
                                        ssp2.restore(newSource2);
                                    }
                                    break;
                                }
                                this.message = "No previous file";
                                break;
                            }
                            case 40: {
                                final int tofile = this.getStrictPositiveNumberInBuffer(1);
                                if (tofile < sources.size()) {
                                    final SavedSourcePositions ssp3 = new SavedSourcePositions((tofile < this.sourceIdx) ? -1 : 0);
                                    this.sourceIdx = tofile;
                                    final String newSource3 = sources.get(this.sourceIdx).getName();
                                    try {
                                        this.openSource();
                                    }
                                    catch (final FileNotFoundException exp3) {
                                        ssp3.restore(newSource3);
                                    }
                                    break;
                                }
                                this.message = "No such file";
                                break;
                            }
                            case 41: {
                                this.message = "FILE_INFO";
                                break;
                            }
                            case 42: {
                                if (sources.size() > 2) {
                                    sources.remove(this.sourceIdx);
                                    if (this.sourceIdx >= sources.size()) {
                                        this.sourceIdx = sources.size() - 1;
                                    }
                                    this.openSource();
                                    break;
                                }
                                break;
                            }
                            case 14: {
                                this.size.copy(this.terminal.getSize());
                                this.display.clear();
                                break;
                            }
                            case 15: {
                                this.message = null;
                                this.size.copy(this.terminal.getSize());
                                this.display.clear();
                                break;
                            }
                            case 0: {
                                this.help();
                                break;
                            }
                        }
                        this.buffer.setLength();
                    }
                    if ((this.quitAtFirstEof && this.nbEof > 0) || (this.quitAtSecondEof && this.nbEof > 1)) {
                        if (this.sourceIdx < sources.size() - 1) {
                            ++this.sourceIdx;
                            this.openSource();
                        }
                        else {
                            op = Operation.EXIT;
                        }
                    }
                    this.display(false);
                } while (op != Operation.EXIT);
            }
            catch (final InterruptedException ex) {}
            finally {
                this.terminal.setAttributes(attr);
                if (prevHandler != null) {
                    this.terminal.handle(Terminal.Signal.WINCH, prevHandler);
                }
                if (!this.noInit) {
                    this.terminal.puts(InfoCmp.Capability.exit_ca_mode, new Object[0]);
                }
                if (!this.noKeypad) {
                    this.terminal.puts(InfoCmp.Capability.keypad_local, new Object[0]);
                }
                this.terminal.writer().flush();
            }
        }
        finally {
            if (this.reader != null) {
                this.reader.close();
            }
            if (status != null) {
                status.restore();
            }
            this.patternHistory.persist();
        }
    }
    
    private void moveToMatch(final boolean forward, final boolean spanFiles) throws IOException {
        if (forward) {
            this.moveToNextMatch(spanFiles);
        }
        else {
            this.moveToPreviousMatch(spanFiles);
        }
    }
    
    private void addSource(final String file) throws IOException {
        if (file.contains("*") || file.contains("?")) {
            for (final Path p : Commands.findFiles(this.currentDir, file)) {
                this.sources.add(new Source.URLSource(p.toUri().toURL(), p.toString()));
            }
        }
        else {
            this.sources.add(new Source.URLSource(this.currentDir.resolve(file).toUri().toURL(), file));
        }
        this.sourceIdx = this.sources.size() - 1;
    }
    
    private void addFile() throws IOException, InterruptedException {
        final KeyMap<Operation> fileKeyMap = new KeyMap<Operation>();
        fileKeyMap.setUnicode(Operation.INSERT);
        for (char i = ' '; i < '\u0100'; ++i) {
            fileKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        fileKeyMap.bind(Operation.RIGHT, KeyMap.key(this.terminal, InfoCmp.Capability.key_right), KeyMap.alt('l'));
        fileKeyMap.bind(Operation.LEFT, KeyMap.key(this.terminal, InfoCmp.Capability.key_left), KeyMap.alt('h'));
        fileKeyMap.bind(Operation.HOME, KeyMap.key(this.terminal, InfoCmp.Capability.key_home), KeyMap.alt('0'));
        fileKeyMap.bind(Operation.END, KeyMap.key(this.terminal, InfoCmp.Capability.key_end), KeyMap.alt('$'));
        fileKeyMap.bind(Operation.BACKSPACE, KeyMap.del());
        fileKeyMap.bind(Operation.DELETE, KeyMap.alt('x'));
        fileKeyMap.bind(Operation.DELETE_WORD, KeyMap.alt('X'));
        fileKeyMap.bind(Operation.DELETE_LINE, KeyMap.ctrl('U'));
        fileKeyMap.bind(Operation.ACCEPT, "\r");
        final SavedSourcePositions ssp = new SavedSourcePositions();
        this.message = null;
        this.buffer.append("Examine: ");
        final int begPos;
        int curPos = begPos = this.buffer.length();
        this.display(false, curPos);
        final LineEditor lineEditor = new LineEditor(begPos);
        while (true) {
            checkInterrupted();
            final Operation op = this.bindingReader.readBinding(fileKeyMap);
            if (op == Operation.ACCEPT) {
                final String name = this.buffer.substring();
                this.addSource(name);
                try {
                    this.openSource();
                }
                catch (final Exception exp) {
                    ssp.restore(name);
                }
                return;
            }
            if (op != null) {
                curPos = lineEditor.editBuffer(op, curPos);
            }
            if (curPos <= begPos) {
                this.buffer.setLength();
                return;
            }
            this.display(false, curPos);
        }
    }
    
    private boolean search() throws IOException, InterruptedException {
        final KeyMap<Operation> searchKeyMap = new KeyMap<Operation>();
        searchKeyMap.setUnicode(Operation.INSERT);
        for (char i = ' '; i < '\u0100'; ++i) {
            searchKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        searchKeyMap.bind(Operation.RIGHT, KeyMap.key(this.terminal, InfoCmp.Capability.key_right), KeyMap.alt('l'));
        searchKeyMap.bind(Operation.LEFT, KeyMap.key(this.terminal, InfoCmp.Capability.key_left), KeyMap.alt('h'));
        searchKeyMap.bind(Operation.NEXT_WORD, KeyMap.alt('w'));
        searchKeyMap.bind(Operation.PREV_WORD, KeyMap.alt('b'));
        searchKeyMap.bind(Operation.HOME, KeyMap.key(this.terminal, InfoCmp.Capability.key_home), KeyMap.alt('0'));
        searchKeyMap.bind(Operation.END, KeyMap.key(this.terminal, InfoCmp.Capability.key_end), KeyMap.alt('$'));
        searchKeyMap.bind(Operation.BACKSPACE, KeyMap.del());
        searchKeyMap.bind(Operation.DELETE, KeyMap.alt('x'));
        searchKeyMap.bind(Operation.DELETE_WORD, KeyMap.alt('X'));
        searchKeyMap.bind(Operation.DELETE_LINE, KeyMap.ctrl('U'));
        searchKeyMap.bind(Operation.UP, KeyMap.key(this.terminal, InfoCmp.Capability.key_up), KeyMap.alt('k'));
        searchKeyMap.bind(Operation.DOWN, KeyMap.key(this.terminal, InfoCmp.Capability.key_down), KeyMap.alt('j'));
        searchKeyMap.bind(Operation.ACCEPT, "\r");
        boolean forward = true;
        this.message = null;
        final int begPos;
        int curPos = begPos = this.buffer.length();
        final char type = this.buffer.charAt();
        String currentBuffer = this.buffer.toString();
        final LineEditor lineEditor = new LineEditor(begPos);
        while (true) {
            checkInterrupted();
            final Operation op;
            switch ((op = this.bindingReader.readBinding(searchKeyMap)).ordinal()) {
                case 56: {
                    this.buffer.setLength();
                    this.buffer.append(type);
                    this.buffer.append(this.patternHistory.up(currentBuffer.substring(1)));
                    curPos = this.buffer.length();
                    break;
                }
                case 57: {
                    this.buffer.setLength();
                    this.buffer.append(type);
                    this.buffer.append(this.patternHistory.down(currentBuffer.substring(1)));
                    curPos = this.buffer.length();
                    break;
                }
                case 55: {
                    try {
                        final String _pattern = this.buffer.substring();
                        if (type == '&') {
                            this.displayPattern = (_pattern.isEmpty() ? null : _pattern);
                            this.getPattern(true);
                        }
                        else {
                            this.pattern = _pattern;
                            this.getPattern();
                            if (type == '/') {
                                this.moveToNextMatch();
                            }
                            else {
                                if (this.lines.size() - this.firstLineToDisplay <= this.size.getRows()) {
                                    this.firstLineToDisplay = this.lines.size();
                                }
                                else {
                                    this.moveForward(this.size.getRows() - 1);
                                }
                                this.moveToPreviousMatch();
                                forward = false;
                            }
                        }
                        this.patternHistory.add(_pattern);
                        this.buffer.setLength();
                    }
                    catch (final PatternSyntaxException e) {
                        String str = e.getMessage();
                        if (str.indexOf(10) > 0) {
                            str = str.substring(0, str.indexOf(10));
                        }
                        if (type == '&') {
                            this.displayPattern = null;
                        }
                        else {
                            this.pattern = null;
                        }
                        this.buffer.setLength();
                        this.message = "Invalid pattern: " + str + " (Press a key)";
                        this.display(false);
                        this.terminal.reader().read();
                        this.message = null;
                    }
                    return forward;
                }
                default: {
                    curPos = lineEditor.editBuffer(op, curPos);
                    currentBuffer = this.buffer.toString();
                    break;
                }
            }
            if (curPos < begPos) {
                this.buffer.setLength();
                return forward;
            }
            this.display(false, curPos);
        }
    }
    
    private void help() throws IOException {
        final SavedSourcePositions ssp = new SavedSourcePositions();
        this.printLineNumbers = false;
        this.sourceIdx = 0;
        try {
            this.openSource();
            this.display(false);
            Operation op;
            do {
                checkInterrupted();
                op = this.bindingReader.readBinding(this.keys, null, false);
                if (op != null) {
                    switch (op.ordinal()) {
                        case 4: {
                            this.moveForward(this.getStrictPositiveNumberInBuffer(this.window));
                            break;
                        }
                        case 5: {
                            this.moveBackward(this.getStrictPositiveNumberInBuffer(this.window));
                            break;
                        }
                    }
                }
                this.display(false);
            } while (op != Operation.EXIT);
        }
        catch (final IOException | InterruptedException ex) {}
        finally {
            ssp.restore(null);
        }
    }
    
    protected void openSource() throws IOException {
        boolean wasOpen = false;
        if (this.reader != null) {
            this.reader.close();
            wasOpen = true;
        }
        boolean displayMessage = false;
        boolean open;
        do {
            final Source source = this.sources.get(this.sourceIdx);
            try {
                final InputStream in = source.read();
                if (this.sources.size() == 2 || this.sourceIdx == 0) {
                    this.message = source.getName();
                }
                else {
                    this.message = source.getName() + " (file " + this.sourceIdx + " of " + (this.sources.size() - 1) + ")";
                }
                this.reader = new BufferedReader(new InputStreamReader(new InterruptibleInputStream(in)));
                this.firstLineInMemory = 0;
                this.lines = new ArrayList<AttributedString>();
                this.firstLineToDisplay = 0;
                this.firstColumnToDisplay = 0;
                this.offsetInLine = 0;
                this.display.clear();
                if (this.sourceIdx == 0) {
                    this.syntaxHighlighter = SyntaxHighlighter.build(this.syntaxFiles, null, "none");
                }
                else {
                    this.syntaxHighlighter = SyntaxHighlighter.build(this.syntaxFiles, source.getName(), this.syntaxName, this.nanorcIgnoreErrors);
                }
                open = true;
                if (!displayMessage) {
                    continue;
                }
                final AttributedStringBuilder asb = new AttributedStringBuilder();
                asb.style(AttributedStyle.INVERSE);
                asb.append(source.getName()).append(" (press RETURN)");
                asb.toAttributedString().println(this.terminal);
                this.terminal.writer().flush();
                this.terminal.reader().read();
            }
            catch (final FileNotFoundException exp) {
                this.sources.remove(this.sourceIdx);
                if (this.sourceIdx > this.sources.size() - 1) {
                    this.sourceIdx = this.sources.size() - 1;
                }
                if (wasOpen) {
                    throw exp;
                }
                final AttributedStringBuilder asb = new AttributedStringBuilder();
                asb.append(source.getName()).append(" not found!");
                asb.toAttributedString().println(this.terminal);
                this.terminal.writer().flush();
                open = false;
                displayMessage = true;
            }
        } while (!open && this.sourceIdx > 0);
        if (!open) {
            throw new FileNotFoundException();
        }
    }
    
    void moveTo(final int lineNum) throws IOException {
        final AttributedString line = this.getLine(lineNum);
        if (line != null) {
            this.display.clear();
            if (this.firstLineInMemory > lineNum) {
                this.openSource();
            }
            this.firstLineToDisplay = lineNum;
            this.offsetInLine = 0;
        }
        else {
            this.message = "Cannot seek to line number " + (lineNum + 1);
        }
    }
    
    private void moveToNextMatch() throws IOException {
        this.moveToNextMatch(false);
    }
    
    private void moveToNextMatch(final boolean spanFiles) throws IOException {
        final Pattern compiled = this.getPattern();
        final Pattern dpCompiled = this.getPattern(true);
        if (compiled != null) {
            int lineNumber = this.firstLineToDisplay + 1;
            while (true) {
                final AttributedString line = this.getLine(lineNumber);
                if (line == null) {
                    break;
                }
                if (this.toBeDisplayed(line, dpCompiled)) {
                    if (compiled.matcher(line).find()) {
                        this.display.clear();
                        this.firstLineToDisplay = lineNumber;
                        this.offsetInLine = 0;
                        return;
                    }
                }
                ++lineNumber;
            }
        }
        if (spanFiles) {
            if (this.sourceIdx < this.sources.size() - 1) {
                final SavedSourcePositions ssp = new SavedSourcePositions();
                final String newSource = this.sources.get(++this.sourceIdx).getName();
                try {
                    this.openSource();
                    this.moveToNextMatch(true);
                }
                catch (final FileNotFoundException exp) {
                    ssp.restore(newSource);
                }
            }
            else {
                this.message = "Pattern not found";
            }
        }
        else {
            this.message = "Pattern not found";
        }
    }
    
    private void moveToPreviousMatch() throws IOException {
        this.moveToPreviousMatch(false);
    }
    
    private void moveToPreviousMatch(final boolean spanFiles) throws IOException {
        final Pattern compiled = this.getPattern();
        final Pattern dpCompiled = this.getPattern(true);
        if (compiled != null) {
            for (int lineNumber = this.firstLineToDisplay - 1; lineNumber >= this.firstLineInMemory; --lineNumber) {
                final AttributedString line = this.getLine(lineNumber);
                if (line == null) {
                    break;
                }
                if (this.toBeDisplayed(line, dpCompiled)) {
                    if (compiled.matcher(line).find()) {
                        this.display.clear();
                        this.firstLineToDisplay = lineNumber;
                        this.offsetInLine = 0;
                        return;
                    }
                }
            }
        }
        if (spanFiles) {
            if (this.sourceIdx > 1) {
                final SavedSourcePositions ssp = new SavedSourcePositions(-1);
                final List<Source> sources = this.sources;
                final int sourceIdx = this.sourceIdx - 1;
                this.sourceIdx = sourceIdx;
                final String newSource = sources.get(sourceIdx).getName();
                try {
                    this.openSource();
                    this.moveTo(Integer.MAX_VALUE);
                    this.moveToPreviousMatch(true);
                }
                catch (final FileNotFoundException exp) {
                    ssp.restore(newSource);
                }
            }
            else {
                this.message = "Pattern not found";
            }
        }
        else {
            this.message = "Pattern not found";
        }
    }
    
    private String printable(final String s) {
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); ++i) {
            final char c = s.charAt(i);
            if (c == '\u001b') {
                sb.append("ESC");
            }
            else if (c < ' ') {
                sb.append('^').append((char)(c + '@'));
            }
            else if (c < '\u0080') {
                sb.append(c);
            }
            else {
                sb.append('\\').append(String.format("%03o", (int)c));
            }
        }
        return sb.toString();
    }
    
    void moveForward(int lines) throws IOException {
        final Pattern dpCompiled = this.getPattern(true);
        final int width = this.size.getColumns() - (this.printLineNumbers ? 8 : 0);
        final int height = this.size.getRows();
        final boolean doOffsets = this.firstColumnToDisplay == 0 && !this.chopLongLines;
        if (lines >= this.size.getRows() - 1) {
            this.display.clear();
        }
        if (lines == Integer.MAX_VALUE) {
            this.moveTo(Integer.MAX_VALUE);
            this.firstLineToDisplay = height - 1;
            for (int l = 0; l < height - 1; ++l) {
                this.firstLineToDisplay = this.prevLine2display(this.firstLineToDisplay, dpCompiled).getU();
            }
        }
        while (--lines >= 0) {
            int lastLineToDisplay = this.firstLineToDisplay;
            if (!doOffsets) {
                for (int i = 0; i < height - 1; ++i) {
                    lastLineToDisplay = this.nextLine2display(lastLineToDisplay, dpCompiled).getU();
                }
            }
            else {
                int off = this.offsetInLine;
                for (int j = 0; j < height - 1; ++j) {
                    final Pair<Integer, AttributedString> nextLine = this.nextLine2display(lastLineToDisplay, dpCompiled);
                    final AttributedString line = nextLine.getV();
                    if (line == null) {
                        lastLineToDisplay = nextLine.getU();
                        break;
                    }
                    if (line.columnLength() > off + width) {
                        off += width;
                    }
                    else {
                        off = 0;
                        lastLineToDisplay = nextLine.getU();
                    }
                }
            }
            if (this.getLine(lastLineToDisplay) == null) {
                this.eof();
                return;
            }
            final Pair<Integer, AttributedString> nextLine2 = this.nextLine2display(this.firstLineToDisplay, dpCompiled);
            final AttributedString line2 = nextLine2.getV();
            if (doOffsets && line2.columnLength() > width + this.offsetInLine) {
                this.offsetInLine += width;
            }
            else {
                this.offsetInLine = 0;
                this.firstLineToDisplay = nextLine2.getU();
            }
        }
    }
    
    void moveBackward(int lines) throws IOException {
        final Pattern dpCompiled = this.getPattern(true);
        final int width = this.size.getColumns() - (this.printLineNumbers ? 8 : 0);
        if (lines >= this.size.getRows() - 1) {
            this.display.clear();
        }
        while (--lines >= 0) {
            if (this.offsetInLine > 0) {
                this.offsetInLine = Math.max(0, this.offsetInLine - width);
            }
            else {
                if (this.firstLineInMemory >= this.firstLineToDisplay) {
                    this.bof();
                    return;
                }
                final Pair<Integer, AttributedString> prevLine = this.prevLine2display(this.firstLineToDisplay, dpCompiled);
                this.firstLineToDisplay = prevLine.getU();
                final AttributedString line = prevLine.getV();
                if (line == null || this.firstColumnToDisplay != 0 || this.chopLongLines) {
                    continue;
                }
                final int length = line.columnLength();
                this.offsetInLine = length - length % width;
            }
        }
    }
    
    private void eof() {
        ++this.nbEof;
        if (this.sourceIdx > 0 && this.sourceIdx < this.sources.size() - 1) {
            this.message = "(END) - Next: " + this.sources.get(this.sourceIdx + 1).getName();
        }
        else {
            this.message = "(END)";
        }
        if (!this.quiet && !this.veryQuiet && !this.quitAtFirstEof && !this.quitAtSecondEof) {
            this.terminal.puts(InfoCmp.Capability.bell, new Object[0]);
            this.terminal.writer().flush();
        }
    }
    
    private void bof() {
        if (!this.quiet && !this.veryQuiet) {
            this.terminal.puts(InfoCmp.Capability.bell, new Object[0]);
            this.terminal.writer().flush();
        }
    }
    
    int getStrictPositiveNumberInBuffer(final int def) {
        try {
            final int n = Integer.parseInt(this.buffer.toString());
            return (n > 0) ? n : def;
        }
        catch (final NumberFormatException e) {
            return def;
        }
        finally {
            this.buffer.setLength();
        }
    }
    
    private Pair<Integer, AttributedString> nextLine2display(int line, final Pattern dpCompiled) throws IOException {
        AttributedString curLine;
        do {
            curLine = this.getLine(line++);
        } while (!this.toBeDisplayed(curLine, dpCompiled));
        return new Pair<Integer, AttributedString>(line, curLine);
    }
    
    private Pair<Integer, AttributedString> prevLine2display(int line, final Pattern dpCompiled) throws IOException {
        AttributedString curLine;
        do {
            curLine = this.getLine(line--);
        } while (line > 0 && !this.toBeDisplayed(curLine, dpCompiled));
        if (line == 0 && !this.toBeDisplayed(curLine, dpCompiled)) {
            curLine = null;
        }
        return new Pair<Integer, AttributedString>(line, curLine);
    }
    
    private boolean toBeDisplayed(final AttributedString curLine, final Pattern dpCompiled) {
        return curLine == null || dpCompiled == null || this.sourceIdx == 0 || dpCompiled.matcher(curLine).find();
    }
    
    synchronized boolean display(final boolean oneScreen) throws IOException {
        return this.display(oneScreen, null);
    }
    
    synchronized boolean display(final boolean oneScreen, final Integer curPos) throws IOException {
        final List<AttributedString> newLines = new ArrayList<AttributedString>();
        final int width = this.size.getColumns() - (this.printLineNumbers ? 8 : 0);
        final int height = this.size.getRows();
        int inputLine = this.firstLineToDisplay;
        AttributedString curLine = null;
        final Pattern compiled = this.getPattern();
        final Pattern dpCompiled = this.getPattern(true);
        boolean fitOnOneScreen = false;
        boolean eof = false;
        if (this.highlight) {
            this.syntaxHighlighter.reset();
            for (int i = Math.max(0, inputLine - height); i < inputLine; ++i) {
                this.syntaxHighlighter.highlight(this.getLine(i));
            }
        }
        for (int terminalLine = 0; terminalLine < height - 1; ++terminalLine) {
            if (curLine == null) {
                final Pair<Integer, AttributedString> nextLine = this.nextLine2display(inputLine, dpCompiled);
                inputLine = nextLine.getU();
                curLine = nextLine.getV();
                if (curLine == null) {
                    if (oneScreen) {
                        fitOnOneScreen = true;
                        break;
                    }
                    eof = true;
                    curLine = new AttributedString("~");
                }
                else if (this.highlight) {
                    curLine = this.syntaxHighlighter.highlight(curLine);
                }
                if (compiled != null) {
                    curLine = curLine.styleMatches(compiled, AttributedStyle.DEFAULT.inverse());
                }
            }
            AttributedString toDisplay;
            if (this.firstColumnToDisplay > 0 || this.chopLongLines) {
                int off = this.firstColumnToDisplay;
                if (terminalLine == 0 && this.offsetInLine > 0) {
                    off = Math.max(this.offsetInLine, off);
                }
                toDisplay = curLine.columnSubSequence(off, off + width);
                curLine = null;
            }
            else {
                if (terminalLine == 0 && this.offsetInLine > 0) {
                    curLine = curLine.columnSubSequence(this.offsetInLine, Integer.MAX_VALUE);
                }
                toDisplay = curLine.columnSubSequence(0, width);
                curLine = curLine.columnSubSequence(width, Integer.MAX_VALUE);
                if (curLine.length() == 0) {
                    curLine = null;
                }
            }
            if (this.printLineNumbers && !eof) {
                final AttributedStringBuilder sb = new AttributedStringBuilder();
                sb.append(String.format("%7d ", inputLine));
                sb.append(toDisplay);
                newLines.add(sb.toAttributedString());
            }
            else {
                newLines.add(toDisplay);
            }
        }
        if (oneScreen) {
            if (fitOnOneScreen) {
                newLines.forEach(l -> l.println(this.terminal));
            }
            return fitOnOneScreen;
        }
        final AttributedStringBuilder msg = new AttributedStringBuilder();
        if ("FILE_INFO".equals(this.message)) {
            final Source source = this.sources.get(this.sourceIdx);
            final Long allLines = source.lines();
            this.message = source.getName() + ((this.sources.size() > 2) ? (" (file " + this.sourceIdx + " of " + (this.sources.size() - 1) + ")") : "") + " lines " + (this.firstLineToDisplay + 1) + "-" + inputLine + "/" + ((allLines != null) ? allLines : ((long)this.lines.size())) + (eof ? " (END)" : "");
        }
        if (this.buffer.length() > 0) {
            msg.append(" ").append(this.buffer);
        }
        else if (!this.bindingReader.getCurrentBuffer().isEmpty() && this.terminal.reader().peek(1L) == -2) {
            msg.append(" ").append(this.printable(this.bindingReader.getCurrentBuffer()));
        }
        else if (this.message != null) {
            msg.style(AttributedStyle.INVERSE);
            msg.append(this.message);
            msg.style(AttributedStyle.INVERSE.inverseOff());
        }
        else if (this.displayPattern != null) {
            msg.append("&");
        }
        else {
            msg.append(":");
        }
        newLines.add(msg.toAttributedString());
        this.display.resize(this.size.getRows(), this.size.getColumns());
        if (curPos == null) {
            this.display.update(newLines, -1);
        }
        else {
            this.display.update(newLines, this.size.cursorPos(this.size.getRows() - 1, curPos + 1));
        }
        return false;
    }
    
    private Pattern getPattern() {
        return this.getPattern(false);
    }
    
    private Pattern getPattern(final boolean doDisplayPattern) {
        Pattern compiled = null;
        final String _pattern = doDisplayPattern ? this.displayPattern : this.pattern;
        if (_pattern != null) {
            final boolean insensitive = this.ignoreCaseAlways || (this.ignoreCaseCond && _pattern.toLowerCase().equals(_pattern));
            compiled = Pattern.compile("(" + _pattern + ")", insensitive ? 66 : 0);
        }
        return compiled;
    }
    
    AttributedString getLine(final int line) throws IOException {
        while (line >= this.lines.size()) {
            final String str = this.reader.readLine();
            if (str == null) {
                break;
            }
            this.lines.add(AttributedString.fromAnsi(str, this.tabs));
        }
        if (line < this.lines.size()) {
            return this.lines.get(line);
        }
        return null;
    }
    
    public static void checkInterrupted() throws InterruptedException {
        Thread.yield();
        if (Thread.currentThread().isInterrupted()) {
            throw new InterruptedException();
        }
    }
    
    private void bindKeys(final KeyMap<Operation> map) {
        map.bind(Operation.HELP, "h", "H");
        map.bind(Operation.EXIT, "q", ":q", "Q", ":Q", "ZZ");
        map.bind(Operation.FORWARD_ONE_LINE, "e", KeyMap.ctrl('E'), "j", KeyMap.ctrl('N'), "\r", KeyMap.key(this.terminal, InfoCmp.Capability.key_down));
        map.bind(Operation.BACKWARD_ONE_LINE, "y", KeyMap.ctrl('Y'), "k", KeyMap.ctrl('K'), KeyMap.ctrl('P'), KeyMap.key(this.terminal, InfoCmp.Capability.key_up));
        map.bind(Operation.FORWARD_ONE_WINDOW_OR_LINES, "f", KeyMap.ctrl('F'), KeyMap.ctrl('V'), " ", KeyMap.key(this.terminal, InfoCmp.Capability.key_npage));
        map.bind(Operation.BACKWARD_ONE_WINDOW_OR_LINES, "b", KeyMap.ctrl('B'), KeyMap.alt('v'), KeyMap.key(this.terminal, InfoCmp.Capability.key_ppage));
        map.bind(Operation.FORWARD_ONE_WINDOW_AND_SET, "z");
        map.bind(Operation.BACKWARD_ONE_WINDOW_AND_SET, "w");
        map.bind(Operation.FORWARD_ONE_WINDOW_NO_STOP, KeyMap.alt(' '));
        map.bind(Operation.FORWARD_HALF_WINDOW_AND_SET, "d", KeyMap.ctrl('D'));
        map.bind(Operation.BACKWARD_HALF_WINDOW_AND_SET, "u", KeyMap.ctrl('U'));
        map.bind(Operation.RIGHT_ONE_HALF_SCREEN, KeyMap.alt(')'), KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        map.bind(Operation.LEFT_ONE_HALF_SCREEN, KeyMap.alt('('), KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        map.bind(Operation.FORWARD_FOREVER, "F");
        map.bind(Operation.REPAINT, "r", KeyMap.ctrl('R'), KeyMap.ctrl('L'));
        map.bind(Operation.REPAINT_AND_DISCARD, "R");
        map.bind(Operation.REPEAT_SEARCH_FORWARD, "n");
        map.bind(Operation.REPEAT_SEARCH_BACKWARD, "N");
        map.bind(Operation.REPEAT_SEARCH_FORWARD_SPAN_FILES, KeyMap.alt('n'));
        map.bind(Operation.REPEAT_SEARCH_BACKWARD_SPAN_FILES, KeyMap.alt('N'));
        map.bind(Operation.UNDO_SEARCH, KeyMap.alt('u'));
        map.bind(Operation.GO_TO_FIRST_LINE_OR_N, "g", "<", KeyMap.alt('<'));
        map.bind(Operation.GO_TO_LAST_LINE_OR_N, "G", ">", KeyMap.alt('>'));
        map.bind(Operation.HOME, KeyMap.key(this.terminal, InfoCmp.Capability.key_home));
        map.bind(Operation.END, KeyMap.key(this.terminal, InfoCmp.Capability.key_end));
        map.bind(Operation.ADD_FILE, ":e", KeyMap.ctrl('X') + KeyMap.ctrl('V'));
        map.bind(Operation.NEXT_FILE, ":n");
        map.bind(Operation.PREV_FILE, ":p");
        map.bind(Operation.GOTO_FILE, ":x");
        map.bind(Operation.INFO_FILE, "=", ":f", KeyMap.ctrl('G'));
        map.bind(Operation.DELETE_FILE, ":d");
        map.bind(Operation.BACKSPACE, KeyMap.del());
        "-/0123456789?&".chars().forEach(c -> map.bind(Operation.CHAR, Character.toString((char)c)));
    }
    
    private class LineEditor
    {
        private final int begPos;
        
        public LineEditor(final int begPos) {
            this.begPos = begPos;
        }
        
        public int editBuffer(final Operation op, int curPos) {
            switch (op.ordinal()) {
                case 44: {
                    Less.this.buffer.insert(curPos++, Less.this.bindingReader.getLastBinding());
                    break;
                }
                case 51: {
                    if (curPos > this.begPos - 1) {
                        Less.this.buffer.deleteCharAt(--curPos);
                        break;
                    }
                    break;
                }
                case 47: {
                    int newPos = Less.this.buffer.length();
                    for (int i = curPos; i < Less.this.buffer.length(); ++i) {
                        if (Less.this.buffer.charAt() == ' ') {
                            newPos = i + 1;
                            break;
                        }
                    }
                    curPos = newPos;
                    break;
                }
                case 48: {
                    int newPos = this.begPos;
                    for (int i = curPos - 2; i > this.begPos; --i) {
                        if (Less.this.buffer.charAt() == ' ') {
                            newPos = i + 1;
                            break;
                        }
                    }
                    curPos = newPos;
                    break;
                }
                case 49: {
                    curPos = this.begPos;
                    break;
                }
                case 50: {
                    curPos = Less.this.buffer.length();
                    break;
                }
                case 52: {
                    if (curPos >= this.begPos && curPos < Less.this.buffer.length()) {
                        Less.this.buffer.deleteCharAt(curPos);
                        break;
                    }
                    break;
                }
                case 53: {
                    while (curPos < Less.this.buffer.length() && Less.this.buffer.charAt() != ' ') {
                        Less.this.buffer.deleteCharAt(curPos);
                    }
                    while (curPos - 1 >= this.begPos) {
                        if (Less.this.buffer.charAt() == ' ') {
                            Less.this.buffer.deleteCharAt(--curPos);
                            break;
                        }
                        Less.this.buffer.deleteCharAt(--curPos);
                    }
                    break;
                }
                case 54: {
                    Less.this.buffer.setLength();
                    curPos = 1;
                    break;
                }
                case 46: {
                    if (curPos > this.begPos) {
                        --curPos;
                        break;
                    }
                    break;
                }
                case 45: {
                    if (curPos < Less.this.buffer.length()) {
                        ++curPos;
                        break;
                    }
                    break;
                }
            }
            return curPos;
        }
    }
    
    private class SavedSourcePositions
    {
        int saveSourceIdx;
        int saveFirstLineToDisplay;
        int saveFirstColumnToDisplay;
        int saveOffsetInLine;
        boolean savePrintLineNumbers;
        
        public SavedSourcePositions(final Less less) {
            this(0);
        }
        
        public SavedSourcePositions(final int dec) {
            this.saveSourceIdx = Less.this.sourceIdx + dec;
            this.saveFirstLineToDisplay = Less.this.firstLineToDisplay;
            this.saveFirstColumnToDisplay = Less.this.firstColumnToDisplay;
            this.saveOffsetInLine = Less.this.offsetInLine;
            this.savePrintLineNumbers = Less.this.printLineNumbers;
        }
        
        public void restore(final String failingSource) throws IOException {
            Less.this.sourceIdx = this.saveSourceIdx;
            Less.this.openSource();
            Less.this.firstLineToDisplay = this.saveFirstLineToDisplay;
            Less.this.firstColumnToDisplay = this.saveFirstColumnToDisplay;
            Less.this.offsetInLine = this.saveOffsetInLine;
            Less.this.printLineNumbers = this.savePrintLineNumbers;
            if (failingSource != null) {
                Less.this.message = failingSource + " not found!";
            }
        }
    }
    
    protected enum Operation
    {
        HELP, 
        EXIT, 
        FORWARD_ONE_LINE, 
        BACKWARD_ONE_LINE, 
        FORWARD_ONE_WINDOW_OR_LINES, 
        BACKWARD_ONE_WINDOW_OR_LINES, 
        FORWARD_ONE_WINDOW_AND_SET, 
        BACKWARD_ONE_WINDOW_AND_SET, 
        FORWARD_ONE_WINDOW_NO_STOP, 
        FORWARD_HALF_WINDOW_AND_SET, 
        BACKWARD_HALF_WINDOW_AND_SET, 
        LEFT_ONE_HALF_SCREEN, 
        RIGHT_ONE_HALF_SCREEN, 
        FORWARD_FOREVER, 
        REPAINT, 
        REPAINT_AND_DISCARD, 
        REPEAT_SEARCH_FORWARD, 
        REPEAT_SEARCH_BACKWARD, 
        REPEAT_SEARCH_FORWARD_SPAN_FILES, 
        REPEAT_SEARCH_BACKWARD_SPAN_FILES, 
        UNDO_SEARCH, 
        GO_TO_FIRST_LINE_OR_N, 
        GO_TO_LAST_LINE_OR_N, 
        GO_TO_PERCENT_OR_N, 
        GO_TO_NEXT_TAG, 
        GO_TO_PREVIOUS_TAG, 
        FIND_CLOSE_BRACKET, 
        FIND_OPEN_BRACKET, 
        OPT_PRINT_LINES, 
        OPT_CHOP_LONG_LINES, 
        OPT_QUIT_AT_FIRST_EOF, 
        OPT_QUIT_AT_SECOND_EOF, 
        OPT_QUIET, 
        OPT_VERY_QUIET, 
        OPT_IGNORE_CASE_COND, 
        OPT_IGNORE_CASE_ALWAYS, 
        OPT_SYNTAX_HIGHLIGHT, 
        ADD_FILE, 
        NEXT_FILE, 
        PREV_FILE, 
        GOTO_FILE, 
        INFO_FILE, 
        DELETE_FILE, 
        CHAR, 
        INSERT, 
        RIGHT, 
        LEFT, 
        NEXT_WORD, 
        PREV_WORD, 
        HOME, 
        END, 
        BACKSPACE, 
        DELETE, 
        DELETE_WORD, 
        DELETE_LINE, 
        ACCEPT, 
        UP, 
        DOWN;
    }
    
    static class InterruptibleInputStream extends FilterInputStream
    {
        InterruptibleInputStream(final InputStream in) {
            super(in);
        }
        
        @Override
        public int read(final byte[] b, final int off, final int len) throws IOException {
            if (Thread.currentThread().isInterrupted()) {
                throw new InterruptedIOException();
            }
            return super.read(b, off, len);
        }
    }
    
    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;
        }
    }
}
