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

org.jline.builtins.Less Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2002-2020, the original author or authors.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package org.jline.builtins;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.jline.builtins.Nano.Parser;
import org.jline.builtins.Nano.PatternHistory;
import org.jline.builtins.Nano.SyntaxHighlighter;
import org.jline.builtins.Source.ResourceSource;
import org.jline.builtins.Source.URLSource;
import org.jline.keymap.BindingReader;
import org.jline.keymap.KeyMap;
import org.jline.terminal.Attributes;
import org.jline.terminal.Size;
import org.jline.terminal.Terminal;
import org.jline.terminal.Terminal.Signal;
import org.jline.terminal.Terminal.SignalHandler;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jline.utils.Display;
import org.jline.utils.InfoCmp.Capability;
import org.jline.utils.NonBlockingReader;
import org.jline.utils.Status;

import static org.jline.keymap.KeyMap.alt;
import static org.jline.keymap.KeyMap.ctrl;
import static org.jline.keymap.KeyMap.del;
import static org.jline.keymap.KeyMap.key;

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 tabs = Collections.singletonList(4);
    protected String syntaxName;
    private String historyLog = null;

    protected final Terminal terminal;
    protected final Display display;
    protected final BindingReader bindingReader;
    protected final Path currentDir;

    protected List sources;
    protected int sourceIdx;
    protected BufferedReader reader;
    protected KeyMap keys;

    protected int firstLineInMemory = 0;
    protected List lines = new ArrayList<>();

    protected int firstLineToDisplay = 0;
    protected int firstColumnToDisplay = 0;
    protected int offsetInLine = 0;

    protected String message;
    protected String errorMessage;
    protected final StringBuilder buffer = new StringBuilder();

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

    protected int window;
    protected int halfWindow;

    protected int nbEof;

    protected PatternHistory patternHistory = new PatternHistory(null);
    protected String pattern;
    protected String displayPattern;

    protected final Size size = new Size();

    SyntaxHighlighter syntaxHighlighter;
    private final List syntaxFiles = new ArrayList<>();
    private boolean highlight = true;
    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(Terminal terminal, Path currentDir) {
        this(terminal, currentDir, null);
    }

    public Less(Terminal terminal, Path currentDir, Options opts) {
        this(terminal, currentDir, opts, null);
    }

    public Less(Terminal terminal, Path currentDir, Options opts, ConfigurationPath configPath) {
        this.terminal = terminal;
        this.display = new Display(terminal, true);
        this.bindingReader = new BindingReader(terminal.reader());
        this.currentDir = currentDir;
        Path lessrc = configPath != null ? configPath.getConfig("jlessrc") : null;
        boolean ignorercfiles = opts!=null && opts.isSet("ignorercfiles");
        if (lessrc != null && !ignorercfiles) {
            try {
                parseConfig(lessrc);
            } catch (IOException e) {
                errorMessage = "Encountered error while reading config file: " + lessrc;
            }
        } else if (new File("/usr/share/nano").exists() && !ignorercfiles) {
            PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:/usr/share/nano/*.nanorc");
            try {
                Files.find(Paths.get("/usr/share/nano"), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path))
                     .forEach(syntaxFiles::add);
                nanorcIgnoreErrors = true;
            } catch (IOException e) {
                errorMessage = "Encountered error while reading nanorc files";
            }
        }
        if (opts != null) {
            if (opts.isSet("QUIT-AT-EOF")){
                quitAtFirstEof = true;
            }
            if (opts.isSet("quit-at-eof")){
                quitAtSecondEof = true;
            }
            if (opts.isSet("quit-if-one-screen")) {
                quitIfOneScreen = true;
            }
            if (opts.isSet("quiet")){
                quiet = true;
            }
            if (opts.isSet("QUIET")){
                veryQuiet = true;
            }
            if (opts.isSet("chop-long-lines")){
                chopLongLines = true;
            }
            if (opts.isSet("IGNORE-CASE")){
                ignoreCaseAlways = true;
            }
            if (opts.isSet("ignore-case")){
                ignoreCaseCond = true;
            }
            if (opts.isSet("LINE-NUMBERS")){
                printLineNumbers = true;
            }
            if (opts.isSet("tabs")) {
                doTabs(opts.get("tabs"));
            }
            if (opts.isSet("syntax")) {
                syntaxName = opts.get("syntax");
                nanorcIgnoreErrors = false;
            }
            if (opts.isSet("no-init")) {
                noInit = true;
            }
            if (opts.isSet("no-keypad")) {
                noKeypad = true;
            }
            if (opts.isSet("historylog")) {
                historyLog = opts.get("historylog");
            }
        }
        if (configPath != null && historyLog != null) {
            try {
                patternHistory = new PatternHistory(configPath.getUserConfig(historyLog, true));
            } catch (IOException e) {
                errorMessage = "Encountered error while reading pattern-history file: " + historyLog;
            }
        }
    }

    private void parseConfig(Path file) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(file.toFile()))) {
            String line = reader.readLine();
            while (line != null) {
                line = line.trim();
                if (line.length() > 0 && !line.startsWith("#")) {
                    List parts = Parser.split(line);
                    if (parts.get(0).equals("include")) {
                        if (parts.get(1).contains("*") || parts.get(1).contains("?")) {
                            PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:" + parts.get(1));
                            Files.find(Paths.get(new File(parts.get(1)).getParent()), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path))
                                    .forEach(syntaxFiles::add);
                        } else {
                            syntaxFiles.add(Paths.get(parts.get(1)));
                        }
                    } else if (parts.size() == 2
                            && (parts.get(0).equals("set") || parts.get(0).equals("unset"))) {
                        String option = parts.get(1);
                        boolean val = parts.get(0).equals("set");
                        if (option.equals("QUIT-AT-EOF")) {
                            quitAtFirstEof = val;
                        } else if (option.equals("quit-at-eof")) {
                            quitAtSecondEof = val;
                        } else if (option.equals("quit-if-one-screen")) {
                            quitIfOneScreen = val;
                        } else if (option.equals("quiet") || option.equals("silent")) {
                            quiet = val;
                        } else if (option.equals("QUIET") || option.equals("SILENT")) {
                            veryQuiet = val;
                        } else if (option.equals("chop-long-lines")) {
                            chopLongLines = val;
                        } else if (option.equals("IGNORE-CASE")) {
                            ignoreCaseAlways = val;
                        } else if (option.equals("ignore-case")) {
                            ignoreCaseCond = val;
                        } else if (option.equals("LINE-NUMBERS")) {
                            printLineNumbers = val;
                        } else {
                            errorMessage = "Less config: Unknown or unsupported configuration option " + option;
                        }
                    } else if (parts.size() == 3 && parts.get(0).equals("set")) {
                        String option = parts.get(1);
                        String val = parts.get(2);
                        if (option.equals("tabs")) {
                            doTabs(val);
                        } else if (option.equals("historylog")) {
                            historyLog = val;
                        } else {
                            errorMessage = "Less config: Unknown or unsupported configuration option " + option;
                        }
                    } else if (parts.get(0).equals("bind") || parts.get(0).equals("unbind")) {
                        errorMessage = "Less config: Key bindings can not be changed!";
                    } else {
                        errorMessage = "Less config: Bad configuration '" + line + "'";
                    }
                }
                line = reader.readLine();
            }
        }
    }

    private void doTabs(String val) {
        tabs = new ArrayList<>();
        for (String s: val.split(",")) {
            try {
                tabs.add(Integer.parseInt(s));
            } catch (Exception ex) {
                errorMessage = "Less config: tabs option error parsing number: " + s;
            }
        }
    }

    // to be removed
    public Less tabs(List tabs) {
        this.tabs = tabs;
        return this;
    }

    public void handle(Signal signal) {
        size.copy(terminal.getSize());
        try {
            display.clear();
            display(false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void run(Source... sources) throws IOException, InterruptedException {
        run(Arrays.asList(sources));
    }

    public void run(List sources) throws IOException, InterruptedException {
        if (sources == null || sources.isEmpty()) {
            throw new IllegalArgumentException("No sources");
        }
        sources.add(0, new ResourceSource("less-help.txt", "HELP -- Press SPACE for more, or q when done"));
        this.sources = sources;

        sourceIdx = 1;
        openSource();
        if (errorMessage != null) {
            message = errorMessage;
            errorMessage = null;
        }
        Status status = Status.getStatus(terminal, false);

        try {
            if (status != null) {
                status.suspend();
            }
            size.copy(terminal.getSize());

            if (quitIfOneScreen && sources.size() == 2) {
                if (display(true)) {
                    return;
                }
            }

            SignalHandler prevHandler = terminal.handle(Signal.WINCH, this::handle);
            Attributes attr = terminal.enterRawMode();
            try {
                window = size.getRows() - 1;
                halfWindow = window / 2;
                keys = new KeyMap<>();
                bindKeys(keys);

                // Use alternate buffer
                if (!noInit) {
                    terminal.puts(Capability.enter_ca_mode);
                }
                if (!noKeypad) {
                    terminal.puts(Capability.keypad_xmit);
                }
                terminal.writer().flush();

                display.clear();
                display(false);
                checkInterrupted();

                options.put("-e", Operation.OPT_QUIT_AT_SECOND_EOF);
                options.put("--quit-at-eof", Operation.OPT_QUIT_AT_SECOND_EOF);
                options.put("-E", Operation.OPT_QUIT_AT_FIRST_EOF);
                options.put("-QUIT-AT-EOF", Operation.OPT_QUIT_AT_FIRST_EOF);
                options.put("-N", Operation.OPT_PRINT_LINES);
                options.put("--LINE-NUMBERS", Operation.OPT_PRINT_LINES);
                options.put("-q", Operation.OPT_QUIET);
                options.put("--quiet", Operation.OPT_QUIET);
                options.put("--silent", Operation.OPT_QUIET);
                options.put("-Q", Operation.OPT_VERY_QUIET);
                options.put("--QUIET", Operation.OPT_VERY_QUIET);
                options.put("--SILENT", Operation.OPT_VERY_QUIET);
                options.put("-S", Operation.OPT_CHOP_LONG_LINES);
                options.put("--chop-long-lines", Operation.OPT_CHOP_LONG_LINES);
                options.put("-i", Operation.OPT_IGNORE_CASE_COND);
                options.put("--ignore-case", Operation.OPT_IGNORE_CASE_COND);
                options.put("-I", Operation.OPT_IGNORE_CASE_ALWAYS);
                options.put("--IGNORE-CASE", Operation.OPT_IGNORE_CASE_ALWAYS);
                options.put("-Y", Operation.OPT_SYNTAX_HIGHLIGHT);
                options.put("--syntax", Operation.OPT_SYNTAX_HIGHLIGHT);

                Operation op;
                boolean forward = true;
                do {
                    checkInterrupted();

                    op = null;
                    //
                    // Option edition
                    //
                    if (buffer.length() > 0 && buffer.charAt(0) == '-') {
                        int c = terminal.reader().read();
                        message = null;
                        if (buffer.length() == 1) {
                            buffer.append((char) c);
                            if (c != '-') {
                                op = options.get(buffer.toString());
                                if (op == null) {
                                    message = "There is no " + printable(buffer.toString()) + " option";
                                    buffer.setLength(0);
                                }
                            }
                        } else if (c == '\r') {
                            op = options.get(buffer.toString());
                            if (op == null) {
                                message = "There is no " + printable(buffer.toString()) + " option";
                                buffer.setLength(0);
                            }
                        } else {
                            buffer.append((char) c);
                            Map matching = new HashMap<>();
                            for (Map.Entry entry : options.entrySet()) {
                                if (entry.getKey().startsWith(buffer.toString())) {
                                    matching.put(entry.getKey(), entry.getValue());
                                }
                            }
                            switch (matching.size()) {
                                case 0:
                                    buffer.setLength(0);
                                    break;
                                case 1:
                                    buffer.setLength(0);
                                    buffer.append(matching.keySet().iterator().next());
                                    break;
                            }
                        }
                    }
                    //
                    // Pattern edition
                    //
                    else if (buffer.length() > 0 && (buffer.charAt(0) == '/' || buffer.charAt(0) == '?' || buffer.charAt(0) == '&')) {
                        forward = search();
                    }
                    //
                    // Command reading
                    //
                    else {
                        Operation obj = bindingReader.readBinding(keys, null, false);
                        if (obj == Operation.CHAR) {
                            char c = bindingReader.getLastBinding().charAt(0);
                            // Enter option mode or pattern edit mode
                            if (c == '-' || c == '/' || c == '?' || c == '&') {
                                buffer.setLength(0);
                            }
                            buffer.append(c);
                        } else if (obj == Operation.BACKSPACE) {
                            if (buffer.length() > 0) {
                                buffer.deleteCharAt(buffer.length() - 1);
                            }
                        } else {
                            op = obj;
                        }
                    }
                    if (op != null) {
                        message = null;
                        switch (op) {
                            case FORWARD_ONE_LINE:
                                moveForward(getStrictPositiveNumberInBuffer(1));
                                break;
                            case BACKWARD_ONE_LINE:
                                moveBackward(getStrictPositiveNumberInBuffer(1));
                                break;
                            case FORWARD_ONE_WINDOW_OR_LINES:
                                moveForward(getStrictPositiveNumberInBuffer(window));
                                break;
                            case FORWARD_ONE_WINDOW_AND_SET:
                                window = getStrictPositiveNumberInBuffer(window);
                                moveForward(window);
                                break;
                            case FORWARD_ONE_WINDOW_NO_STOP:
                                moveForward(window);
                                // TODO: handle no stop
                                break;
                            case FORWARD_HALF_WINDOW_AND_SET:
                                halfWindow = getStrictPositiveNumberInBuffer(halfWindow);
                                moveForward(halfWindow);
                                break;
                            case BACKWARD_ONE_WINDOW_AND_SET:
                                window = getStrictPositiveNumberInBuffer(window);
                                moveBackward(window);
                                break;
                            case BACKWARD_ONE_WINDOW_OR_LINES:
                                moveBackward(getStrictPositiveNumberInBuffer(window));
                                break;
                            case BACKWARD_HALF_WINDOW_AND_SET:
                                halfWindow = getStrictPositiveNumberInBuffer(halfWindow);
                                moveBackward(halfWindow);
                                break;
                            case GO_TO_FIRST_LINE_OR_N:
                                moveTo(getStrictPositiveNumberInBuffer(1) - 1);
                                break;
                            case GO_TO_LAST_LINE_OR_N:
                                int lineNum = getStrictPositiveNumberInBuffer(0) - 1;
                                if (lineNum < 0) {
                                    moveForward(Integer.MAX_VALUE);
                                } else {
                                    moveTo(lineNum);
                                }
                                break;
                            case HOME:
                                moveTo(0);
                                break;
                            case END:
                                moveForward(Integer.MAX_VALUE);
                                break;
                            case LEFT_ONE_HALF_SCREEN:
                                firstColumnToDisplay = Math.max(0, firstColumnToDisplay - size.getColumns() / 2);
                                break;
                            case RIGHT_ONE_HALF_SCREEN:
                                firstColumnToDisplay += size.getColumns() / 2;
                                break;
                            case REPEAT_SEARCH_BACKWARD_SPAN_FILES:
                                moveToMatch(!forward, true);
                                break;
                            case REPEAT_SEARCH_BACKWARD:
                                moveToMatch(!forward, false);
                                break;
                            case REPEAT_SEARCH_FORWARD_SPAN_FILES:
                                moveToMatch(forward, true);
                                break;
                            case REPEAT_SEARCH_FORWARD:
                                moveToMatch(forward, false);
                                break;
                            case UNDO_SEARCH:
                                pattern = null;
                                break;
                            case OPT_PRINT_LINES:
                                buffer.setLength(0);
                                printLineNumbers = !printLineNumbers;
                                message = printLineNumbers ? "Constantly display line numbers" : "Don't use line numbers";
                                break;
                            case OPT_QUIET:
                                buffer.setLength(0);
                                quiet = !quiet;
                                veryQuiet = false;
                                message = quiet ? "Ring the bell for errors but not at eof/bof" : "Ring the bell for errors AND at eof/bof";
                                break;
                            case OPT_VERY_QUIET:
                                buffer.setLength(0);
                                veryQuiet = !veryQuiet;
                                quiet = false;
                                message = veryQuiet ? "Never ring the bell" : "Ring the bell for errors AND at eof/bof";
                                break;
                            case OPT_CHOP_LONG_LINES:
                                buffer.setLength(0);
                                offsetInLine = 0;
                                chopLongLines = !chopLongLines;
                                message = chopLongLines ? "Chop long lines" : "Fold long lines";
                                display.clear();
                                break;
                            case OPT_IGNORE_CASE_COND:
                                ignoreCaseCond = !ignoreCaseCond;
                                ignoreCaseAlways = false;
                                message = ignoreCaseCond ? "Ignore case in searches" : "Case is significant in searches";
                                break;
                            case OPT_IGNORE_CASE_ALWAYS:
                                ignoreCaseAlways = !ignoreCaseAlways;
                                ignoreCaseCond = false;
                                message = ignoreCaseAlways ? "Ignore case in searches and in patterns" : "Case is significant in searches";
                                break;
                            case OPT_SYNTAX_HIGHLIGHT:
                                highlight = !highlight;
                                message = "Highlight " + (highlight ? "enabled" : "disabled");
                                break;
                            case ADD_FILE:
                                addFile();
                                break;
                            case NEXT_FILE:
                                int next = getStrictPositiveNumberInBuffer(1);
                                if (sourceIdx < sources.size() - next) {
                                    SavedSourcePositions ssp = new SavedSourcePositions();
                                    sourceIdx += next;
                                    String newSource = sources.get(sourceIdx).getName();
                                    try {
                                        openSource();
                                    } catch (FileNotFoundException exp) {
                                        ssp.restore(newSource);
                                    }
                                } else {
                                    message = "No next file";
                                }
                                break;
                            case PREV_FILE:
                                int prev = getStrictPositiveNumberInBuffer(1);
                                if (sourceIdx > prev) {
                                    SavedSourcePositions ssp = new SavedSourcePositions(-1);
                                    sourceIdx -= prev;
                                    String newSource = sources.get(sourceIdx).getName();
                                    try {
                                        openSource();
                                    } catch (FileNotFoundException exp) {
                                        ssp.restore(newSource);
                                    }
                                } else {
                                    message = "No previous file";
                                }
                                break;
                            case GOTO_FILE:
                                int tofile = getStrictPositiveNumberInBuffer(1);
                                if (tofile < sources.size()) {
                                    SavedSourcePositions ssp = new SavedSourcePositions(tofile < sourceIdx ? -1 : 0);
                                    sourceIdx = tofile;
                                    String newSource = sources.get(sourceIdx).getName();
                                    try {
                                        openSource();
                                    } catch (FileNotFoundException exp) {
                                        ssp.restore(newSource);
                                    }
                                } else {
                                    message = "No such file";
                                }
                                break;
                            case INFO_FILE:
                                message = MESSAGE_FILE_INFO;
                                break;
                            case DELETE_FILE:
                                if (sources.size() > 2) {
                                    sources.remove(sourceIdx);
                                    if (sourceIdx >= sources.size()) {
                                        sourceIdx = sources.size() - 1;
                                    }
                                    openSource();
                                }
                                break;
                            case REPAINT:
                                size.copy(terminal.getSize());
                                display.clear();
                                break;
                            case REPAINT_AND_DISCARD:
                                message = null;
                                size.copy(terminal.getSize());
                                display.clear();
                                break;
                            case HELP:
                                help();
                                break;
                        }
                        buffer.setLength(0);
                    }
                    if (quitAtFirstEof && nbEof > 0 || quitAtSecondEof && nbEof > 1) {
                        if (sourceIdx < sources.size() - 1) {
                            sourceIdx++;
                            openSource();
                        } else {
                            op = Operation.EXIT;
                        }
                    }
                    display(false);
                } while (op != Operation.EXIT);
            } catch (InterruptedException ie) {
                // Do nothing
            } finally {
                terminal.setAttributes(attr);
                if (prevHandler != null) {
                    terminal.handle(Terminal.Signal.WINCH, prevHandler);
                }
                // Use main buffer
                if (!noInit) {
                    terminal.puts(Capability.exit_ca_mode);
                }
                if (!noKeypad) {
                    terminal.puts(Capability.keypad_local);
                }
                terminal.writer().flush();
            }
        } finally {
            if (reader != null) {
                reader.close();
            }
            if (status != null) {
                status.restore();
            }
            patternHistory.persist();
        }
    }

    private void moveToMatch(boolean forward, boolean spanFiles) throws IOException {
        if (forward) {
            moveToNextMatch(spanFiles);
        } else {
            moveToPreviousMatch(spanFiles);
        }
    }

    private class LineEditor {
        private final int begPos;

        public LineEditor(int begPos) {
            this.begPos = begPos;
        }

        public int editBuffer(Operation op, int curPos) {
            switch (op) {
            case INSERT:
                buffer.insert(curPos++, bindingReader.getLastBinding());
                break;
            case BACKSPACE:
                if (curPos > begPos - 1) {
                    buffer.deleteCharAt(--curPos);
                }
                break;
            case NEXT_WORD:
                int newPos = buffer.length();
                for (int i = curPos; i < buffer.length(); i++) {
                    if (buffer.charAt(i) == ' ') {
                        newPos = i + 1;
                        break;
                    }
                }
                curPos = newPos;
                break;
            case PREV_WORD:
                newPos = begPos;
                for (int i = curPos - 2; i > begPos; i--) {
                    if (buffer.charAt(i) == ' ') {
                        newPos = i + 1;
                        break;
                    }
                }
                curPos = newPos;
                break;
            case HOME:
                curPos = begPos;
                break;
            case END:
                curPos = buffer.length();
                break;
            case DELETE:
                if (curPos >= begPos && curPos < buffer.length()) {
                    buffer.deleteCharAt(curPos);
                }
                break;
            case DELETE_WORD:
                while (true) {
                    if(curPos < buffer.length() && buffer.charAt(curPos) != ' '){
                        buffer.deleteCharAt(curPos);
                    } else {
                        break;
                    }
                }
                while (true) {
                    if(curPos - 1 >= begPos) {
                        if (buffer.charAt(curPos - 1) != ' ') {
                            buffer.deleteCharAt(--curPos);
                        } else {
                            buffer.deleteCharAt(--curPos);
                            break;
                        }
                    } else {
                        break;
                    }
                }
                break;
            case DELETE_LINE:
                buffer.setLength(begPos);
                curPos = 1;
                break;
            case LEFT:
                if (curPos > begPos) {
                    curPos--;
                }
                break;
            case RIGHT:
                if (curPos < buffer.length()) {
                    curPos++;
                }
                break;
            }
            return curPos;
        }
    }

    private class SavedSourcePositions {
        int saveSourceIdx;
        int saveFirstLineToDisplay;
        int saveFirstColumnToDisplay;
        int saveOffsetInLine;
        boolean savePrintLineNumbers;

        public SavedSourcePositions (){
            this(0);
        }
        public SavedSourcePositions (int dec){
            saveSourceIdx = sourceIdx + dec;
            saveFirstLineToDisplay = firstLineToDisplay;
            saveFirstColumnToDisplay = firstColumnToDisplay;
            saveOffsetInLine = offsetInLine;
            savePrintLineNumbers = printLineNumbers;
        }

        public void restore(String failingSource) throws IOException {
            sourceIdx = saveSourceIdx;
            openSource();
            firstLineToDisplay = saveFirstLineToDisplay;
            firstColumnToDisplay = saveFirstColumnToDisplay;
            offsetInLine = saveOffsetInLine;
            printLineNumbers = savePrintLineNumbers;
            if (failingSource != null) {
                message = failingSource + " not found!";
            }
        }
    }

    private void addSource(String file) throws IOException {
        if (file.contains("*") || file.contains("?")) {
            for (Path p: Commands.findFiles(currentDir, file)) {
                 sources.add(new URLSource(p.toUri().toURL(), p.toString()));
            }
        } else {
            sources.add(new URLSource(currentDir.resolve(file).toUri().toURL(), file));
        }
        sourceIdx = sources.size() - 1;
    }

    private void addFile() throws IOException, InterruptedException {
        KeyMap fileKeyMap = new KeyMap<>();
        fileKeyMap.setUnicode(Operation.INSERT);
        for (char i = 32; i < 256; i++) {
            fileKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        fileKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right), alt('l'));
        fileKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left), alt('h'));
        fileKeyMap.bind(Operation.HOME, key(terminal, Capability.key_home), alt('0'));
        fileKeyMap.bind(Operation.END, key(terminal, Capability.key_end), alt('$'));
        fileKeyMap.bind(Operation.BACKSPACE, del());
        fileKeyMap.bind(Operation.DELETE, alt('x'));
        fileKeyMap.bind(Operation.DELETE_WORD, alt('X'));
        fileKeyMap.bind(Operation.DELETE_LINE, ctrl('U'));
        fileKeyMap.bind(Operation.ACCEPT, "\r");

        SavedSourcePositions ssp = new SavedSourcePositions();
        message = null;
        buffer.append("Examine: ");
        int curPos = buffer.length();
        final int begPos = curPos;
        display(false, curPos);
        LineEditor lineEditor = new LineEditor(begPos);
        while (true) {
            checkInterrupted();
            Operation op;
            switch (op = bindingReader.readBinding(fileKeyMap)) {
            case ACCEPT:
                String name = buffer.substring(begPos);
                addSource(name);
                try {
                    openSource();
                } catch (Exception exp) {
                    ssp.restore(name);
                }
                return;
            default:
                curPos = lineEditor.editBuffer(op, curPos);
                break;
            }
            if (curPos > begPos) {
                display(false, curPos);
            } else {
                buffer.setLength(0);
                return;
            }
        }
    }

    private boolean search() throws IOException, InterruptedException {
        KeyMap searchKeyMap = new KeyMap<>();
        searchKeyMap.setUnicode(Operation.INSERT);
        for (char i = 32; i < 256; i++) {
            searchKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        searchKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right), alt('l'));
        searchKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left), alt('h'));
        searchKeyMap.bind(Operation.NEXT_WORD, alt('w'));
        searchKeyMap.bind(Operation.PREV_WORD, alt('b'));
        searchKeyMap.bind(Operation.HOME, key(terminal, Capability.key_home), alt('0'));
        searchKeyMap.bind(Operation.END, key(terminal, Capability.key_end), alt('$'));
        searchKeyMap.bind(Operation.BACKSPACE, del());
        searchKeyMap.bind(Operation.DELETE, alt('x'));
        searchKeyMap.bind(Operation.DELETE_WORD, alt('X'));
        searchKeyMap.bind(Operation.DELETE_LINE, ctrl('U'));
        searchKeyMap.bind(Operation.UP, key(terminal, Capability.key_up), alt('k'));
        searchKeyMap.bind(Operation.DOWN, key(terminal, Capability.key_down), alt('j'));
        searchKeyMap.bind(Operation.ACCEPT, "\r");

        boolean forward = true;
        message = null;
        int curPos = buffer.length();
        final int begPos = curPos;
        final char type = buffer.charAt(0);
        String currentBuffer = buffer.toString();
        LineEditor lineEditor = new LineEditor(begPos);
        while (true) {
            checkInterrupted();
            Operation op;
            switch (op = bindingReader.readBinding(searchKeyMap)) {
            case UP:
                buffer.setLength(0);
                buffer.append(type);
                buffer.append(patternHistory.up(currentBuffer.substring(1)));
                curPos = buffer.length();
                break;
            case DOWN:
                buffer.setLength(0);
                buffer.append(type);
                buffer.append(patternHistory.down(currentBuffer.substring(1)));
                curPos = buffer.length();
                break;
            case ACCEPT:
                try {
                    String _pattern = buffer.substring(1);
                    if (type == '&') {
                        displayPattern = _pattern.length() > 0 ? _pattern : null;
                        getPattern(true);
                    } else {
                        pattern = _pattern;
                        getPattern();
                        if (type == '/') {
                            moveToNextMatch();
                        } else {
                            if (lines.size() - firstLineToDisplay <= size.getRows() ) {
                                firstLineToDisplay = lines.size();
                            } else {
                                moveForward(size.getRows() - 1);
                            }
                            moveToPreviousMatch();
                            forward = false;
                        }
                    }
                    patternHistory.add(_pattern);
                    buffer.setLength(0);
                } catch (PatternSyntaxException e) {
                    String str = e.getMessage();
                    if (str.indexOf('\n') > 0) {
                        str = str.substring(0, str.indexOf('\n'));
                    }
                    if (type == '&') {
                        displayPattern = null;
                    } else {
                        pattern = null;
                    }
                    buffer.setLength(0);
                    message = "Invalid pattern: " + str + " (Press a key)";
                    display(false);
                    terminal.reader().read();
                    message = null;
                }
                return forward;
            default:
                curPos = lineEditor.editBuffer(op, curPos);
                currentBuffer = buffer.toString();
                break;
            }
            if (curPos < begPos) {
                buffer.setLength(0);
                return forward;
            } else {
                display(false, curPos);
            }
        }
    }

    private void help() throws IOException {
        SavedSourcePositions ssp = new SavedSourcePositions();
        printLineNumbers = false;
        sourceIdx = 0;
        try {
            openSource();
            display(false);
            Operation op;
            do {
                checkInterrupted();
                op = bindingReader.readBinding(keys, null, false);
                if (op != null) {
                    switch (op) {
                    case FORWARD_ONE_WINDOW_OR_LINES:
                        moveForward(getStrictPositiveNumberInBuffer(window));
                        break;
                    case BACKWARD_ONE_WINDOW_OR_LINES:
                        moveBackward(getStrictPositiveNumberInBuffer(window));
                        break;
                    }
                }
                display(false);
            } while (op != Operation.EXIT);
        } catch (IOException|InterruptedException exp) {
            // Do nothing
        } finally {
            ssp.restore(null);
        }
    }

    protected void openSource() throws IOException {
        boolean wasOpen = false;
        if (reader != null) {
            reader.close();
            wasOpen = true;
        }
        boolean open;
        boolean displayMessage = false;
        do {
            Source source = sources.get(sourceIdx);
            try {
                InputStream in = source.read();
                if (sources.size() == 2 || sourceIdx == 0) {
                    message = source.getName();
                } else {
                    message = source.getName() + " (file " + sourceIdx + " of "
                            + (sources.size() - 1) + ")";
                }
                reader = new BufferedReader(new InputStreamReader(
                        new InterruptibleInputStream(in)));
                firstLineInMemory = 0;
                lines = new ArrayList<>();
                firstLineToDisplay = 0;
                firstColumnToDisplay = 0;
                offsetInLine = 0;
                display.clear();
                if (sourceIdx == 0) {
                    syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, null, "none");
                } else {
                    syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, source.getName(), syntaxName, nanorcIgnoreErrors);
                }
                open = true;
                if (displayMessage) {
                    AttributedStringBuilder asb = new AttributedStringBuilder();
                    asb.style(AttributedStyle.INVERSE);
                    asb.append(source.getName() + " (press RETURN)");
                    asb.toAttributedString().println(terminal);
                    terminal.writer().flush();
                    terminal.reader().read();
                }
            } catch (FileNotFoundException exp) {
                sources.remove(sourceIdx);
                if (sourceIdx > sources.size() - 1) {
                    sourceIdx = sources.size() - 1;
                }
                if (wasOpen) {
                    throw exp;
                } else {
                    AttributedStringBuilder asb = new AttributedStringBuilder();
                    asb.append(source.getName() + " not found!");
                    asb.toAttributedString().println(terminal);
                    terminal.writer().flush();
                    open = false;
                    displayMessage = true;
                }
            }
        } while (!open && sourceIdx > 0);
        if (!open) {
            throw new FileNotFoundException();
        }
    }

    void moveTo(int lineNum) throws IOException {
        AttributedString line = getLine(lineNum);
        if (line != null){
            display.clear();
            if (firstLineInMemory > lineNum) {
                openSource();
            }
            firstLineToDisplay = lineNum;
            offsetInLine = 0;
        } else {
            message = "Cannot seek to line number " + (lineNum + 1);
        }
    }

    private void moveToNextMatch() throws IOException {
        moveToNextMatch(false);
    }

    private void moveToNextMatch(boolean spanFiles) throws IOException {
        Pattern compiled = getPattern();
        Pattern dpCompiled = getPattern(true);
        if (compiled != null) {
            for (int lineNumber = firstLineToDisplay + 1; ; lineNumber++) {
                AttributedString line = getLine(lineNumber);
                if (line == null) {
                    break;
                } else if (!toBeDisplayed(line, dpCompiled)) {
                    continue;
                } else if (compiled.matcher(line).find()) {
                    display.clear();
                    firstLineToDisplay = lineNumber;
                    offsetInLine = 0;
                    return;
                }
            }
        }
        if (spanFiles) {
            if (sourceIdx < sources.size() - 1) {
                SavedSourcePositions ssp = new SavedSourcePositions();
                String newSource = sources.get(++sourceIdx).getName();
                try {
                    openSource();
                    moveToNextMatch(true);
                } catch (FileNotFoundException exp) {
                    ssp.restore(newSource);
                }
            } else {
                message = "Pattern not found";
            }
        } else {
            message = "Pattern not found";
        }
    }

    private void moveToPreviousMatch() throws IOException {
        moveToPreviousMatch(false);
    }

    private void moveToPreviousMatch(boolean spanFiles) throws IOException {
        Pattern compiled = getPattern();
        Pattern dpCompiled = getPattern(true);
        if (compiled != null) {
            for (int lineNumber = firstLineToDisplay - 1; lineNumber >= firstLineInMemory; lineNumber--) {
                AttributedString line = getLine(lineNumber);
                if (line == null) {
                    break;
                } else if (!toBeDisplayed(line, dpCompiled)) {
                    continue;
                } else if (compiled.matcher(line).find()) {
                    display.clear();
                    firstLineToDisplay = lineNumber;
                    offsetInLine = 0;
                    return;
                }
            }
        }
        if (spanFiles) {
            if (sourceIdx > 1) {
                SavedSourcePositions ssp = new SavedSourcePositions(-1);
                String newSource = sources.get(--sourceIdx).getName();
                try {
                    openSource();
                    firstLineToDisplay = (int)(long)sources.get(sourceIdx).lines();
                    moveToPreviousMatch(true);
                } catch (FileNotFoundException exp) {
                    ssp.restore(newSource);
                }
            } else {
                message = "Pattern not found";
            }
        } else {
            message = "Pattern not found";
        }
    }

    private String printable(String s) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == ESCAPE) {
                sb.append("ESC");
            } else if (c < 32) {
                sb.append('^').append((char) (c + '@'));
            } else if (c < 128) {
                sb.append(c);
            } else {
                sb.append('\\').append(String.format("%03o", (int) c));
            }
        }
        return sb.toString();
    }

    void moveForward(int lines) throws IOException {
        Pattern dpCompiled = getPattern(true);
        int width = size.getColumns() - (printLineNumbers ? 8 : 0);
        int height = size.getRows();
        boolean doOffsets = firstColumnToDisplay == 0 && !chopLongLines;
        if (lines >= size.getRows() - 1) {
            display.clear();
        }
        if (lines == Integer.MAX_VALUE) {
            Long allLines = sources.get(sourceIdx).lines();
            if (allLines != null) {
                firstLineToDisplay = (int)(long)allLines;
                for (int l = 0; l < height - 1; l++) {
                    firstLineToDisplay = prevLine2display(firstLineToDisplay, dpCompiled).getU();
                }
            }
        }
        while (--lines >= 0) {
            int lastLineToDisplay = firstLineToDisplay;
            if (!doOffsets) {
                for (int l = 0; l < height - 1; l++) {
                    lastLineToDisplay = nextLine2display(lastLineToDisplay, dpCompiled).getU();
                }
            } else {
                int off = offsetInLine;
                for (int l = 0; l < height - 1; l++) {
                    Pair nextLine = nextLine2display(lastLineToDisplay, dpCompiled);
                    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 (getLine(lastLineToDisplay) == null) {
                eof();
                return;
            }
            Pair nextLine = nextLine2display(firstLineToDisplay, dpCompiled);
            AttributedString line = nextLine.getV();
            if (doOffsets && line.columnLength() > width + offsetInLine) {
                offsetInLine += width;
            } else {
                offsetInLine = 0;
                firstLineToDisplay = nextLine.getU();
            }
        }
    }

    void moveBackward(int lines) throws IOException {
        Pattern dpCompiled = getPattern(true);
        int width = size.getColumns() - (printLineNumbers ? 8 : 0);
        if (lines >= size.getRows() - 1) {
            display.clear();
        }
        while (--lines >= 0) {
            if (offsetInLine > 0) {
                offsetInLine = Math.max(0, offsetInLine - width);
            } else if (firstLineInMemory < firstLineToDisplay) {
                Pair prevLine = prevLine2display(firstLineToDisplay, dpCompiled);
                firstLineToDisplay = prevLine.getU();
                AttributedString line = prevLine.getV();
                if (line != null && firstColumnToDisplay == 0 && !chopLongLines) {
                    int length = line.columnLength();
                    offsetInLine = length - length % width;
                }
            } else {
                bof();
                return;
            }
        }
    }

    private void eof() {
        nbEof++;
        if (sourceIdx > 0 && sourceIdx < sources.size() - 1) {
            message = "(END) - Next: " + sources.get(sourceIdx + 1).getName();
        } else {
            message = "(END)";
        }
        if (!quiet && !veryQuiet && !quitAtFirstEof && !quitAtSecondEof) {
            terminal.puts(Capability.bell);
            terminal.writer().flush();
        }
    }

    private void bof() {
        if (!quiet && !veryQuiet) {
            terminal.puts(Capability.bell);
            terminal.writer().flush();
        }
    }

    int getStrictPositiveNumberInBuffer(int def) {
        try {
            int n = Integer.parseInt(buffer.toString());
            return (n > 0) ? n : def;
        } catch (NumberFormatException e) {
            return def;
        } finally {
            buffer.setLength(0);
        }
    }

    private Pair nextLine2display(int line, Pattern dpCompiled) throws IOException {
        AttributedString curLine;
        do {
            curLine = getLine(line++);
        } while (!toBeDisplayed(curLine, dpCompiled));
        return new Pair<>(line, curLine);
    }

    private Pair prevLine2display(int line, Pattern dpCompiled) throws IOException {
        AttributedString curLine;
        do {
            curLine = getLine(line--);
        } while (line > 0 && !toBeDisplayed(curLine, dpCompiled));
        if (line == 0 && !toBeDisplayed(curLine, dpCompiled)) {
            curLine = null;
        }
        return new Pair<>(line, curLine);
    }

    private boolean toBeDisplayed(AttributedString curLine, Pattern dpCompiled) {
        return curLine == null || dpCompiled == null || sourceIdx == 0 || dpCompiled.matcher(curLine).find();
    }

    synchronized boolean display(boolean oneScreen) throws IOException {
        return display(oneScreen, null);
    }

    synchronized boolean display(boolean oneScreen, Integer curPos) throws IOException {
        List newLines = new ArrayList<>();
        int width = size.getColumns() - (printLineNumbers ? 8 : 0);
        int height = size.getRows();
        int inputLine = firstLineToDisplay;
        AttributedString curLine = null;
        Pattern compiled = getPattern();
        Pattern dpCompiled = getPattern(true);
        boolean fitOnOneScreen = false;
        boolean eof = false;
        syntaxHighlighter.reset();
        for (int terminalLine = 0; terminalLine < height - 1; terminalLine++) {
            if (curLine == null) {
                Pair nextLine = nextLine2display(inputLine, dpCompiled);
                inputLine = nextLine.getU();
                curLine = nextLine.getV();
                if (curLine == null) {
                    if (oneScreen) {
                        fitOnOneScreen = true;
                        break;
                    }
                    eof = true;
                    curLine = new AttributedString("~");
                } else if (highlight) {
                    curLine = syntaxHighlighter.highlight(curLine);
                }
                if (compiled != null) {
                    curLine = curLine.styleMatches(compiled, AttributedStyle.DEFAULT.inverse());
                }
            }
            AttributedString toDisplay;
            if (firstColumnToDisplay > 0 || chopLongLines) {
                int off = firstColumnToDisplay;
                if (terminalLine == 0 && offsetInLine > 0) {
                    off = Math.max(offsetInLine, off);
                }
                toDisplay = curLine.columnSubSequence(off, off + width);
                curLine = null;
            } else {
                if (terminalLine == 0 && offsetInLine > 0) {
                    curLine = curLine.columnSubSequence(offsetInLine, Integer.MAX_VALUE);
                }
                toDisplay = curLine.columnSubSequence(0, width);
                curLine = curLine.columnSubSequence(width, Integer.MAX_VALUE);
                if (curLine.length() == 0) {
                    curLine = null;
                }
            }
            if (printLineNumbers && !eof) {
                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(terminal));
            }
            return fitOnOneScreen;
        }
        AttributedStringBuilder msg = new AttributedStringBuilder();
        if (MESSAGE_FILE_INFO.equals(message)){
            Source source = sources.get(sourceIdx);
            Long allLines = source.lines();
            message = source.getName()
                    + (sources.size() > 2 ? " (file " + sourceIdx + " of " + (sources.size() - 1) + ")" : "")
                    + " lines " + (firstLineToDisplay + 1) + "-" + inputLine + "/" + (allLines != null ? allLines : lines.size())
                    + (eof ? " (END)" : "");
        }
        if (buffer.length() > 0) {
            msg.append(" ").append(buffer);
        } else if (bindingReader.getCurrentBuffer().length() > 0
                && terminal.reader().peek(1) == NonBlockingReader.READ_EXPIRED) {
            msg.append(" ").append(printable(bindingReader.getCurrentBuffer()));
        } else if (message != null) {
            msg.style(AttributedStyle.INVERSE);
            msg.append(message);
            msg.style(AttributedStyle.INVERSE.inverseOff());
        } else if (displayPattern != null) {
            msg.append("&");
        } else {
            msg.append(":");
        }
        newLines.add(msg.toAttributedString());

        display.resize(size.getRows(), size.getColumns());
        if (curPos == null) {
            display.update(newLines, -1);
        } else {
            display.update(newLines, size.cursorPos(size.getRows() - 1, curPos + 1));
        }
        return false;
    }

    private Pattern getPattern() {
        return getPattern(false);
    }

    private Pattern getPattern(boolean doDisplayPattern) {
        Pattern compiled = null;
        String _pattern = doDisplayPattern ? displayPattern : pattern;
        if (_pattern != null) {
            boolean insensitive = ignoreCaseAlways || ignoreCaseCond && _pattern.toLowerCase().equals(_pattern);
            compiled = Pattern.compile("(" + _pattern + ")", insensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE : 0);
        }
        return compiled;
    }

    AttributedString getLine(int line) throws IOException {
        while (line >= lines.size()) {
            String str = reader.readLine();
            if (str != null) {
                lines.add(AttributedString.fromAnsi(str, tabs));
            } else {
                break;
            }
        }
        if (line < lines.size()) {
            return lines.get(line);
        }
        return null;
    }

    /**
     * This is for long running commands to be interrupted by ctrl-c
     *
     * @throws InterruptedException if the thread has been interruped
     */
    public static void checkInterrupted() throws InterruptedException {
        Thread.yield();
        if (Thread.currentThread().isInterrupted()) {
            throw new InterruptedException();
        }
    }

    private void bindKeys(KeyMap map) {
        map.bind(Operation.HELP, "h", "H");
        map.bind(Operation.EXIT, "q", ":q", "Q", ":Q", "ZZ");
        map.bind(Operation.FORWARD_ONE_LINE, "e", ctrl('E'), "j", ctrl('N'), "\r", key(terminal, Capability.key_down));
        map.bind(Operation.BACKWARD_ONE_LINE, "y", ctrl('Y'), "k", ctrl('K'), ctrl('P'), key(terminal, Capability.key_up));
        map.bind(Operation.FORWARD_ONE_WINDOW_OR_LINES, "f", ctrl('F'), ctrl('V'), " ", key(terminal, Capability.key_npage));
        map.bind(Operation.BACKWARD_ONE_WINDOW_OR_LINES, "b", ctrl('B'), alt('v'), key(terminal, 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, alt(' '));
        map.bind(Operation.FORWARD_HALF_WINDOW_AND_SET, "d", ctrl('D'));
        map.bind(Operation.BACKWARD_HALF_WINDOW_AND_SET, "u", ctrl('U'));
        map.bind(Operation.RIGHT_ONE_HALF_SCREEN, alt(')'), key(terminal, Capability.key_right));
        map.bind(Operation.LEFT_ONE_HALF_SCREEN, alt('('), key(terminal, Capability.key_left));
        map.bind(Operation.FORWARD_FOREVER, "F");
        map.bind(Operation.REPAINT, "r", ctrl('R'), 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, alt('n'));
        map.bind(Operation.REPEAT_SEARCH_BACKWARD_SPAN_FILES, alt('N'));
        map.bind(Operation.UNDO_SEARCH, alt('u'));
        map.bind(Operation.GO_TO_FIRST_LINE_OR_N, "g", "<", alt('<'));
        map.bind(Operation.GO_TO_LAST_LINE_OR_N, "G", ">", alt('>'));
        map.bind(Operation.HOME, key(terminal, Capability.key_home));
        map.bind(Operation.END, key(terminal, Capability.key_end));
        map.bind(Operation.ADD_FILE, ":e", ctrl('X') + 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", ctrl('G'));
        map.bind(Operation.DELETE_FILE, ":d");
        map.bind(Operation.BACKSPACE, del());
        "-/0123456789?&".chars().forEach(c -> map.bind(Operation.CHAR, Character.toString((char) c)));
    }

    protected enum Operation {

        // General
        HELP,
        EXIT,

        // Moving
        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,

        // Searching
        REPEAT_SEARCH_FORWARD,
        REPEAT_SEARCH_BACKWARD,
        REPEAT_SEARCH_FORWARD_SPAN_FILES,
        REPEAT_SEARCH_BACKWARD_SPAN_FILES,
        UNDO_SEARCH,

        // Jumping
        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,

        // Options
        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,

        // Files
        ADD_FILE,
        NEXT_FILE,
        PREV_FILE,
        GOTO_FILE,
        INFO_FILE,
        DELETE_FILE,

        //
        CHAR,

        // Edit pattern
        INSERT,
        RIGHT,
        LEFT,
        NEXT_WORD,
        PREV_WORD,
        HOME,
        END,
        BACKSPACE,
        DELETE,
        DELETE_WORD,
        DELETE_LINE,
        ACCEPT,
        UP,
        DOWN
    }

    static class InterruptibleInputStream extends FilterInputStream {
        InterruptibleInputStream(InputStream in) {
            super(in);
        }

        @Override
        public int read(byte[] b, int off, int len) throws IOException {
            if (Thread.currentThread().isInterrupted()) {
                throw new InterruptedIOException();
            }
            return super.read(b, off, len);
        }
    }

    static class Pair {
        final U u; final V v;
        public Pair(U u, V v) {
            this.u = u;
            this.v = v;
        }
        public U getU() {
            return u;
        }
        public V getV() {
            return v;
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy