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

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

/*
 * Copyright (c) 2002-2021, 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.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URL;
import java.nio.charset.Charset;
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.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.jline.keymap.BindingReader;
import org.jline.keymap.KeyMap;
import org.jline.reader.Editor;
import org.jline.terminal.Attributes;
import org.jline.terminal.Attributes.ControlChar;
import org.jline.terminal.Attributes.InputFlag;
import org.jline.terminal.Attributes.LocalFlag;
import org.jline.terminal.MouseEvent;
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.*;
import org.jline.utils.InfoCmp.Capability;
import org.mozilla.universalchardet.UniversalDetector;

import static org.jline.keymap.KeyMap.KEYMAP_LENGTH;
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 Nano implements Editor {

    // Final fields
    protected final Terminal terminal;
    protected final Display display;
    protected final BindingReader bindingReader;
    protected final Size size;
    protected final Path root;
    protected final int vsusp;
    private final List syntaxFiles = new ArrayList<>();

    // Keys
    protected KeyMap keys;

    // Configuration
    public String title = "JLine Nano 3.0.0";
    public boolean printLineNumbers = false;
    public boolean wrapping = false;
    public boolean smoothScrolling = true;
    public boolean mouseSupport = false;
    public boolean oneMoreLine = true;
    public boolean constantCursor = false;
    public boolean quickBlank = false;
    public int tabs = 4;
    public String brackets = "\"’)>]}";
    public String matchBrackets = "(<[{)>]}";
    public String punct = "!.?";
    public String quoteStr = "^([ \\t]*[#:>\\|}])+";
    private boolean restricted = false;
    private String syntaxName;
    private boolean writeBackup = false;
    private boolean atBlanks = false;
    private boolean view = false;
    private boolean cut2end = false;
    private boolean tempFile = false;
    private String historyLog = null;
    private boolean tabsToSpaces = false;
    private boolean autoIndent = false;

    // Input
    protected final List buffers = new ArrayList<>();
    protected int bufferIndex;
    protected Buffer buffer;

    protected String message;
    protected String errorMessage = null;
    protected int nbBindings = 0;

    protected LinkedHashMap shortcuts;

    protected String editMessage;
    protected final StringBuilder editBuffer = new StringBuilder();

    protected boolean searchCaseSensitive;
    protected boolean searchRegexp;
    protected boolean searchBackwards;
    protected String searchTerm;
    protected int matchedLength = -1;
    protected PatternHistory patternHistory = new PatternHistory(null);
    protected WriteMode writeMode = WriteMode.WRITE;
    protected List cutbuffer = new ArrayList<>();
    protected boolean mark = false;
    protected boolean highlight = true;
    private boolean searchToReplace = false;
    protected boolean readNewBuffer = true;
    private boolean nanorcIgnoreErrors;
    private final boolean windowsTerminal;

    protected enum WriteMode {
        WRITE,
        APPEND,
        PREPEND
    }

    protected enum WriteFormat {
        UNIX,
        DOS,
        MAC
    }

    protected enum CursorMovement {
        RIGHT,
        LEFT,
        STILL
    }

    public static String[] usage() {
        return new String[]{
                "nano -  edit files",
                "Usage: nano [OPTIONS] [FILES]",
                "  -? --help                    Show help",
                "  -B --backup                  When saving a file, back up the previous version of it, using the current filename",
                "                               suffixed with a tilde (~)." ,
                "  -I --ignorercfiles           Don't look at the system's nanorc nor at the user's nanorc." ,
                "  -Q --quotestr=regex          Set the regular expression for matching the quoting part of a line.",
                "  -T --tabsize=number          Set the size (width) of a tab to number columns.",
                "  -U --quickblank              Do quick status-bar blanking: status-bar messages will disappear after 1 keystroke.",
                "  -c --constantshow            Constantly show the cursor position on the status bar.",
                "  -e --emptyline               Do not use the line below the title bar, leaving it entirely blank.",
                "  -j --jumpyscrolling          Scroll the buffer contents per half-screen instead of per line.",
                "  -l --linenumbers             Display line numbers to the left of the text area.",
                "  -m --mouse                   Enable mouse support, if available for your system.",
                "  -$ --softwrap                Enable 'soft wrapping'. ",
                "  -a --atblanks                Wrap lines at whitespace instead of always at the edge of the screen.",
                "  -R --restricted              Restricted mode: don't allow suspending; don't allow a file to be appended to,",
                "                               prepended to, or saved under a different name if it already has one;",
                "                               and don't use backup files.",
                "  -Y --syntax=name             The name of the syntax highlighting to use.",
                "  -z --suspend                 Enable the ability to suspend nano using the system's suspend keystroke (usually ^Z).",
                "  -v --view                    Don't allow the contents of the file to be altered: read-only mode.",
                "  -k --cutfromcursor           Make the 'Cut Text' command cut from the current cursor position to the end of the line",
                "  -t --tempfile                Save a changed buffer without prompting (when exiting with ^X).",
                "  -H --historylog=name         Log search strings to file, so they can be retrieved in later sessions",
                "  -E --tabstospaces            Convert typed tabs to spaces.",
                "  -i --autoindent              Indent new lines to the previous line's indentation."
        };
    }

    protected class Buffer {
        String file;
        Charset charset;
        WriteFormat format = WriteFormat.UNIX;
        List lines;

        int firstLineToDisplay;
        int firstColumnToDisplay = 0;
        int offsetInLineToDisplay;

        int line;
        List> offsets = new ArrayList<>();
        int offsetInLine;
        int column;
        int wantedColumn;
        boolean uncut = false;
        int[] markPos = {-1, -1}; // line, offsetInLine + column
        SyntaxHighlighter syntaxHighlighter;

        boolean dirty;

        protected Buffer(String file) {
            this.file = file;
            this.syntaxHighlighter = SyntaxHighlighter.build(syntaxFiles, file, syntaxName, nanorcIgnoreErrors);
        }

        void open() throws IOException {
            if (lines != null) {
                return;
            }

            lines = new ArrayList<>();
            lines.add("");
            charset = Charset.defaultCharset();
            computeAllOffsets();

            if (file == null) {
                return;
            }

            Path path = root.resolve(file);
            if (Files.isDirectory(path)) {
                setMessage("\"" + file + "\" is a directory");
                return;
            }

            try (InputStream fis = Files.newInputStream(path))
            {
                read(fis);
            } catch (IOException e) {
                setMessage("Error reading " + file + ": " + e.getMessage());
            }
        }

        void open(InputStream is) throws IOException {
            if (lines != null) {
                return;
            }

            lines = new ArrayList<>();
            lines.add("");
            charset = Charset.defaultCharset();
            computeAllOffsets();

            read(is);
        }

        void read(InputStream fis) throws IOException {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buffer = new byte[4096];
            int remaining;
            while ((remaining = fis.read(buffer)) > 0) {
                bos.write(buffer, 0, remaining);
            }
            byte[] bytes = bos.toByteArray();

            try {
                UniversalDetector detector = new UniversalDetector(null);
                detector.handleData(bytes, 0, bytes.length);
                detector.dataEnd();
                if (detector.getDetectedCharset() != null) {
                    charset = Charset.forName(detector.getDetectedCharset());
                }
            } catch (Throwable t) {
                // Ignore
            }

            // TODO: detect format, do not eat last newline
            try (BufferedReader reader = new BufferedReader(
                    new InputStreamReader(new ByteArrayInputStream(bytes), charset))) {
                String line;
                lines.clear();
                while ((line = reader.readLine()) != null) {
                    lines.add(line);
                }
            }
            if (lines.isEmpty()) {
                lines.add("");
            }
            computeAllOffsets();
            moveToChar(0);
        }

        private int charPosition(int displayPosition) {
            return charPosition(line, displayPosition, CursorMovement.STILL);
        }

        private int charPosition(int displayPosition, CursorMovement move) {
            return charPosition(line, displayPosition, move);
        }

        private int charPosition(int line, int displayPosition) {
            return charPosition(line, displayPosition, CursorMovement.STILL);
        }

        private int charPosition(int line, int displayPosition, CursorMovement move) {
            int out = lines.get(line).length();
            if (!lines.get(line).contains("\t") || displayPosition == 0) {
                out = displayPosition;
            } else if (displayPosition < length(lines.get(line))) {
                int rdiff = 0;
                int ldiff = 0;
                for (int i = 0; i < lines.get(line).length(); i++) {
                    int dp = length(lines.get(line).substring(0, i));
                    if (move == CursorMovement.LEFT) {
                        if (dp <= displayPosition) {
                            out = i;
                        } else {
                            break;
                        }
                    } else if (move == CursorMovement.RIGHT) {
                        if (dp >= displayPosition) {
                            out = i;
                            break;
                        }
                    } else if (move == CursorMovement.STILL) {
                        if (dp <= displayPosition) {
                            ldiff = displayPosition - dp;
                            out = i;
                        } else if (dp >= displayPosition) {
                            rdiff = dp - displayPosition;
                            if (rdiff < ldiff) {
                                out = i;
                            }
                            break;
                        }
                    }
                }
            }
            return out;
        }

        String blanks(int nb) {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < nb; i++) {
                sb.append(' ');
            }
            return sb.toString();
        }

        void insert(String insert) {
            String text = lines.get(line);
            int pos = charPosition(offsetInLine + column);
            insert = insert.replaceAll("\r\n", "\n");
            insert = insert.replaceAll("\r", "\n");
            if (tabsToSpaces && insert.length() == 1 && insert.charAt(0) == '\t') {
                int len = pos == text.length() ? length(text + insert) : length(text.substring(0, pos) + insert);
                insert = blanks(len - offsetInLine - column);
            }
            if (autoIndent && insert.length() == 1 && insert.charAt(0) == '\n') {
                for (char c : lines.get(line).toCharArray()) {
                    if (c == ' ') {
                        insert += c;
                    } else if (c == '\t') {
                        insert += c;
                    } else {
                        break;
                    }
                }
            }
            String mod;
            String tail = "";
            if (pos == text.length()) {
                mod = text + insert;
            } else {
                mod = text.substring(0, pos) + insert;
                tail = text.substring(pos);
            }
            List ins = new ArrayList<>();
            int last = 0;
            int idx = mod.indexOf('\n', last);
            while (idx >= 0) {
                ins.add(mod.substring(last, idx));
                last = idx + 1;
                idx = mod.indexOf('\n', last);
            }
            ins.add(mod.substring(last) + tail);
            int curPos = length(mod.substring(last));
            lines.set(line, ins.get(0));
            offsets.set(line, computeOffsets(ins.get(0)));
            for (int i = 1; i < ins.size(); i++) {
                ++line;
                lines.add(line, ins.get(i));
                offsets.add(line, computeOffsets(ins.get(i)));
            }
            moveToChar(curPos);
            ensureCursorVisible();
            dirty = true;
        }

        void computeAllOffsets() {
            offsets.clear();
            for (String text : lines) {
                offsets.add(computeOffsets(text));
            }
        }

        LinkedList computeOffsets(String line) {
            String text = new AttributedStringBuilder().tabs(tabs).append(line).toString();
            int width = size.getColumns() - (printLineNumbers ? 8 : 0);
            LinkedList offsets = new LinkedList<>();
            offsets.add(0);
            if (wrapping) {
                int last = 0;
                int prevword = 0;
                boolean inspace = false;
                for (int i = 0; i < text.length(); i++) {
                    if (isBreakable(text.charAt(i))) {
                        inspace = true;
                    } else if (inspace) {
                        prevword = i;
                        inspace = false;
                    }
                    if (i == last + width - 1) {
                        if (prevword == last) {
                            prevword = i;
                        }
                        offsets.add(prevword);
                        last = prevword;
                    }
                }
            }
            return offsets;
        }

        boolean isBreakable(char ch) {
            return !atBlanks || ch == ' ';
        }

        void moveToChar(int pos) {
            moveToChar(pos, CursorMovement.STILL);
        }

        void moveToChar(int pos, CursorMovement move) {
            if (!wrapping) {
                if (pos > column && pos - firstColumnToDisplay + 1 > width()) {
                    firstColumnToDisplay = offsetInLine + column - 6;
                } else if (pos < column && firstColumnToDisplay + 5 > pos) {
                    firstColumnToDisplay = Math.max(0, firstColumnToDisplay - width() + 5);
                }
            }
            if (lines.get(line).contains("\t")) {
                int cpos = charPosition(pos, move);
                if (cpos < lines.get(line).length()) {
                    pos = length(lines.get(line).substring(0, cpos));
                } else {
                    pos = length(lines.get(line));
                }
            }
            offsetInLine = prevLineOffset(line, pos + 1).get();
            column = pos - offsetInLine;
        }

        void delete(int count) {
            while (--count >= 0 && moveRight(1) && backspace(1));
        }

        boolean backspace(int count) {
            while (count > 0) {
                String text = lines.get(line);
                int pos = charPosition(offsetInLine + column);
                if (pos == 0) {
                    if (line == 0) {
                        bof();
                        return false;
                    }
                    String prev = lines.get(--line);
                    lines.set(line, prev + text);
                    offsets.set(line, computeOffsets(prev + text));
                    moveToChar(length(prev));
                    lines.remove(line + 1);
                    offsets.remove(line + 1);
                    count--;
                } else {
                    int nb = Math.min(pos, count);
                    int curPos = length(text.substring(0, pos - nb));
                    text = text.substring(0, pos - nb) + text.substring(pos);
                    lines.set(line, text);
                    offsets.set(line, computeOffsets(text));
                    moveToChar(curPos);
                    count -= nb;
                }
                dirty = true;
            }
            ensureCursorVisible();
            return true;
        }

        boolean moveLeft(int chars) {
            boolean ret = true;
            while (--chars >= 0) {
                if (offsetInLine + column > 0) {
                    moveToChar(offsetInLine + column - 1, CursorMovement.LEFT);
                } else if (line > 0) {
                    line--;
                    moveToChar(length(getLine(line)));
                } else {
                    bof();
                    ret = false;
                    break;
                }
            }
            wantedColumn = column;
            ensureCursorVisible();
            return ret;
        }

        boolean moveRight(int chars) {
            return moveRight(chars, false);
        }

        int width() {
            return size.getColumns() - (printLineNumbers ? 8 : 0) - (wrapping ? 0 : 1) - (firstColumnToDisplay > 0 ? 1 : 0);
        }

        boolean moveRight(int chars, boolean fromBeginning) {
            if (fromBeginning) {
                firstColumnToDisplay = 0;
                offsetInLine = 0;
                column = 0;
                chars = Math.min(chars, length(getLine(line)));
            }
            boolean ret = true;
            while (--chars >= 0) {
                int len =  length(getLine(line));
                if (offsetInLine + column + 1 <= len) {
                    moveToChar(offsetInLine + column + 1, CursorMovement.RIGHT);
                } else if (getLine(line + 1) != null) {
                    line++;
                    firstColumnToDisplay = 0;
                    offsetInLine = 0;
                    column = 0;
                } else {
                    eof();
                    ret = false;
                    break;
                }
            }
            wantedColumn = column;
            ensureCursorVisible();
            return ret;
        }

        void moveDown(int lines) {
            cursorDown(lines);
            ensureCursorVisible();
        }

        void moveUp(int lines) {
            cursorUp(lines);
            ensureCursorVisible();
        }

        private Optional prevLineOffset(int line, int offsetInLine) {
            if (line >= offsets.size()) {
                return Optional.empty();
            }
            Iterator it = offsets.get(line).descendingIterator();
            while (it.hasNext()) {
                int off = it.next();
                if (off < offsetInLine) {
                    return Optional.of(off);
                }
            }
            return Optional.empty();
        }

        private Optional nextLineOffset(int line, int offsetInLine) {
            if (line >= offsets.size()) {
                return Optional.empty();
            }
            return offsets.get(line).stream()
                    .filter(o -> o > offsetInLine)
                    .findFirst();
        }

        void moveDisplayDown(int lines) {
            int height = size.getRows() - computeHeader().size() - computeFooter().size();
            // Adjust cursor
            while (--lines >= 0) {
                int lastLineToDisplay = firstLineToDisplay;
                if (!wrapping) {
                    lastLineToDisplay += height - 1;
                } else {
                    int off = offsetInLineToDisplay;
                    for (int l = 0; l < height - 1; l++) {
                        Optional next = nextLineOffset(lastLineToDisplay, off);
                        if (next.isPresent()) {
                            off = next.get();
                        } else {
                            off = 0;
                            lastLineToDisplay++;
                        }
                    }
                }
                if (getLine(lastLineToDisplay) == null) {
                    eof();
                    return;
                }
                Optional next = nextLineOffset(firstLineToDisplay, offsetInLineToDisplay);
                if (next.isPresent()) {
                    offsetInLineToDisplay = next.get();
                } else {
                    offsetInLineToDisplay = 0;
                    firstLineToDisplay++;
                }
            }
        }

        void moveDisplayUp(int lines) {
            int width = size.getColumns() - (printLineNumbers ? 8 : 0);
            while (--lines >= 0) {
                if (offsetInLineToDisplay > 0) {
                    offsetInLineToDisplay = Math.max(0, offsetInLineToDisplay - (width - 1));
                } else if (firstLineToDisplay > 0) {
                    firstLineToDisplay--;
                    offsetInLineToDisplay = prevLineOffset(firstLineToDisplay, Integer.MAX_VALUE).get();
                } else {
                    bof();
                    return;
                }
            }
        }

        private void cursorDown(int lines) {
            // Adjust cursor
            firstColumnToDisplay = 0;
            while (--lines >= 0) {
                if (!wrapping) {
                    if (getLine(line + 1) != null) {
                        line++;
                        offsetInLine = 0;
                        column = Math.min(length(getLine(line)), wantedColumn);
                    } else {
                        bof();
                        break;
                    }
                } else {
                    String txt = getLine(line);
                    Optional off = nextLineOffset(line, offsetInLine);
                    if (off.isPresent()) {
                        offsetInLine = off.get();
                    } else if (getLine(line + 1) == null) {
                        eof();
                        break;
                    } else {
                        line++;
                        offsetInLine = 0;
                        txt = getLine(line);
                    }
                    int next = nextLineOffset(line, offsetInLine).orElse(length(txt));
                    column = Math.min(wantedColumn, next - offsetInLine);
                }
            }
            moveToChar(offsetInLine + column);
        }

        private void cursorUp(int lines) {
            firstColumnToDisplay = 0;
            while (--lines >= 0) {
                if (!wrapping) {
                    if (line > 0) {
                        line--;
                        column = Math.min(length(getLine(line)) - offsetInLine, wantedColumn);
                    } else {
                        bof();
                        break;
                    }
                } else {
                    Optional prev = prevLineOffset(line, offsetInLine);
                    if (prev.isPresent()) {
                        offsetInLine = prev.get();
                    } else if (line > 0) {
                        line--;
                        offsetInLine = prevLineOffset(line, Integer.MAX_VALUE).get();
                        int next = nextLineOffset(line, offsetInLine).orElse(length(getLine(line)));
                        column = Math.min(wantedColumn, next - offsetInLine);
                    } else {
                        bof();
                        break;
                    }
                }
            }
            moveToChar(offsetInLine + column);
        }

        void ensureCursorVisible() {
            List header = computeHeader();
            int rwidth = size.getColumns();
            int height = size.getRows() - header.size() - computeFooter().size();

            while (line < firstLineToDisplay
                    || line == firstLineToDisplay && offsetInLine < offsetInLineToDisplay) {
                moveDisplayUp(smoothScrolling ? 1 : height / 2);
            }

            while (true) {
                int cursor = computeCursorPosition(header.size() * size.getColumns() + (printLineNumbers ? 8 : 0), rwidth);
                if (cursor >= (height + header.size()) * rwidth) {
                    moveDisplayDown(smoothScrolling ? 1 : height / 2);
                } else {
                    break;
                }
            }
        }

        void eof() {
        }

        void bof() {
        }

        void resetDisplay() {
            column = offsetInLine + column;
            moveRight(column, true);
        }

        String getLine(int line) {
            return line < lines.size() ? lines.get(line) : null;
        }

        String getTitle() {
            return file != null ? "File: " + file : "New Buffer";
        }

        List computeHeader() {
            String left = Nano.this.getTitle();
            String middle = null;
            String right = dirty ? "Modified" : "        ";

            int width = size.getColumns();
            int mstart = 2 + left.length() + 1;
            int mend = width - 2 - 8;

            if (file == null) {
                middle = "New Buffer";
            } else {
                int max = mend - mstart;
                String src = file;
                if ("File: ".length() + src.length() > max) {
                    int lastSep = src.lastIndexOf('/');
                    if (lastSep > 0) {
                        String p1 = src.substring(lastSep);
                        String p0 = src.substring(0, lastSep);
                        while (p0.startsWith(".")) {
                            p0 = p0.substring(1);
                        }
                        int nb = max - p1.length() - "File: ...".length();
                        int cut;
                        cut = Math.max(0, Math.min(p0.length(), p0.length() - nb));
                        middle = "File: ..." + p0.substring(cut) + p1;
                    }
                    if (middle == null || middle.length() > max) {
                        left = null;
                        max = mend - 2;
                        int nb = max - "File: ...".length();
                        int cut = Math.max(0, Math.min(src.length(), src.length() - nb));
                        middle = "File: ..." + src.substring(cut);
                        if (middle.length() > max) {
                            middle = middle.substring(0, max);
                        }
                    }
                } else {
                    middle = "File: " + src;
                }
            }

            int pos = 0;
            AttributedStringBuilder sb = new AttributedStringBuilder();
            sb.style(AttributedStyle.INVERSE);
            sb.append("  ");
            pos += 2;

            if (left != null) {
                sb.append(left);
                pos += left.length();
                sb.append(" ");
                pos += 1;
                for (int i = 1; i < (size.getColumns() - middle.length()) / 2 - left.length() - 1 - 2; i++) {
                    sb.append(" ");
                    pos++;
                }
            }
            sb.append(middle);
            pos += middle.length();
            while (pos < width - 8 - 2) {
                sb.append(" ");
                pos++;
            }
            sb.append(right);
            sb.append("  \n");
            if (oneMoreLine) {
                return Collections.singletonList(sb.toAttributedString());
            } else {
                return Arrays.asList(sb.toAttributedString(), new AttributedString("\n"));
            }
        }

        void highlightDisplayedLine(int curLine, int curOffset, int nextOffset, AttributedStringBuilder line) {
            AttributedString disp = highlight ? syntaxHighlighter.highlight(new AttributedStringBuilder().tabs(tabs).append(getLine(curLine)))
                                              : new AttributedStringBuilder().tabs(tabs).append(getLine(curLine)).toAttributedString();
            int[] hls = highlightStart();
            int[] hle = highlightEnd();
            if (hls[0] == -1 || hle[0] == -1) {
                line.append(disp.columnSubSequence(curOffset, nextOffset));
            } else if (hls[0] == hle[0]) {
                if (curLine == hls[0]) {
                    if (hls[1] > nextOffset) {
                        line.append(disp.columnSubSequence(curOffset, nextOffset));
                    } else if (hls[1] <  curOffset) {
                        if (hle[1] > nextOffset) {
                            line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE);
                        } else if (hle[1] > curOffset) {
                            line.append(disp.columnSubSequence(curOffset, hle[1]), AttributedStyle.INVERSE);
                            line.append(disp.columnSubSequence(hle[1], nextOffset));
                        } else {
                            line.append(disp.columnSubSequence(curOffset, nextOffset));
                        }
                    } else {
                        line.append(disp.columnSubSequence(curOffset, hls[1]));
                        if (hle[1] > nextOffset) {
                            line.append(disp.columnSubSequence(hls[1], nextOffset), AttributedStyle.INVERSE);
                        } else {
                            line.append(disp.columnSubSequence(hls[1], hle[1]), AttributedStyle.INVERSE);
                            line.append(disp.columnSubSequence(hle[1], nextOffset));
                        }
                    }
                } else {
                    line.append(disp.columnSubSequence(curOffset, nextOffset));
                }
            } else {
                if (curLine > hls[0] && curLine < hle[0]) {
                    line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE);
                } else if (curLine == hls[0]) {
                    if (hls[1] > nextOffset) {
                        line.append(disp.columnSubSequence(curOffset, nextOffset));
                    } else if (hls[1] < curOffset) {
                        line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE);
                    } else {
                        line.append(disp.columnSubSequence(curOffset, hls[1]));
                        line.append(disp.columnSubSequence(hls[1], nextOffset), AttributedStyle.INVERSE);
                    }
                } else if (curLine == hle[0]) {
                    if (hle[1] < curOffset) {
                        line.append(disp.columnSubSequence(curOffset, nextOffset));
                    } else if (hle[1] > nextOffset) {
                        line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE);
                    } else {
                        line.append(disp.columnSubSequence(curOffset, hle[1]), AttributedStyle.INVERSE);
                        line.append(disp.columnSubSequence(hle[1], nextOffset));
                    }
                } else {
                    line.append(disp.columnSubSequence(curOffset, nextOffset));
                }
            }
        }

        List getDisplayedLines(int nbLines) {
            AttributedStyle s = AttributedStyle.DEFAULT.foreground(AttributedStyle.BLACK + AttributedStyle.BRIGHT);
            AttributedString cut = new AttributedString("…", s);
            AttributedString ret = new AttributedString("↩", s);

            List newLines = new ArrayList<>();
            int rwidth = size.getColumns();
            int width = rwidth - (printLineNumbers ? 8 : 0);
            int curLine = firstLineToDisplay;
            int curOffset = offsetInLineToDisplay;
            int prevLine = -1;
            syntaxHighlighter.reset();
            for (int terminalLine = 0; terminalLine < nbLines; terminalLine++) {
                AttributedStringBuilder line = new AttributedStringBuilder().tabs(tabs);
                if (printLineNumbers && curLine < lines.size()) {
                    line.style(s);
                    if (curLine != prevLine) {
                        line.append(String.format("%7d ", curLine + 1));
                    } else {
                        line.append("      ‧ ");
                    }
                    line.style(AttributedStyle.DEFAULT);
                    prevLine = curLine;
                }
                if (curLine >= lines.size()) {
                    // Nothing to do
                } else if (!wrapping) {
                    AttributedString disp = new AttributedStringBuilder().tabs(tabs).append(getLine(curLine)).toAttributedString();
                    if (this.line == curLine) {
                        int cutCount = 1;
                        if (firstColumnToDisplay > 0) {
                            line.append(cut);
                            cutCount = 2;
                        }
                        if (disp.columnLength() - firstColumnToDisplay >= width - (cutCount - 1)*cut.columnLength()) {
                            highlightDisplayedLine(curLine, firstColumnToDisplay
                                , firstColumnToDisplay + width - cutCount*cut.columnLength(), line);
                            line.append(cut);
                        } else {
                            highlightDisplayedLine(curLine, firstColumnToDisplay, disp.columnLength(), line);
                        }
                    } else {
                        if (disp.columnLength() >= width) {
                            highlightDisplayedLine(curLine, 0, width - cut.columnLength(), line);
                            line.append(cut);
                        } else {
                            highlightDisplayedLine(curLine, 0, disp.columnLength(), line);
                        }
                    }
                    curLine++;
                } else {
                    Optional nextOffset = nextLineOffset(curLine, curOffset);
                    if (nextOffset.isPresent()) {
                        highlightDisplayedLine(curLine, curOffset, nextOffset.get(), line);
                        line.append(ret);
                        curOffset = nextOffset.get();
                    } else {
                        highlightDisplayedLine(curLine, curOffset, Integer.MAX_VALUE, line);
                        curLine++;
                        curOffset = 0;
                    }
                }
                line.append('\n');
                newLines.add(line.toAttributedString());
            }
            return newLines;
        }

        public void moveTo(int x, int y) {
            if (printLineNumbers) {
                x = Math.max(x - 8, 0);
            }
            line = firstLineToDisplay;
            offsetInLine = offsetInLineToDisplay;
            wantedColumn = x;
            cursorDown(y);
        }

        public void gotoLine(int x, int y) {
            line = y < lines.size() ? y : lines.size() - 1;
            x = Math.min(x, length(lines.get(line)));
            firstLineToDisplay = line > 0 ? line - 1 : line;
            offsetInLine = 0;
            offsetInLineToDisplay = 0;
            column = 0;
            moveRight(x);
        }

        public int getDisplayedCursor() {
            return computeCursorPosition(printLineNumbers ? 8 : 0, size.getColumns() + 1);
        }

        private int computeCursorPosition(int cursor, int rwidth) {
            int cur = firstLineToDisplay;
            int off = offsetInLineToDisplay;
            while (true) {
                if (cur < line || off < offsetInLine) {
                    if (!wrapping) {
                        cursor += rwidth;
                        cur++;
                    } else {
                        cursor += rwidth;
                        Optional next = nextLineOffset(cur, off);
                        if (next.isPresent()) {
                            off = next.get();
                        } else {
                            cur++;
                            off = 0;
                        }
                    }
                } else if (cur == line) {
                    if (!wrapping && column > firstColumnToDisplay + width()) {
                        while (column > firstColumnToDisplay + width()) {
                            firstColumnToDisplay += width();
                        }
                    }
                    cursor += column - firstColumnToDisplay + (firstColumnToDisplay > 0 ? 1 : 0);
                    break;
                } else {
                    throw new IllegalStateException();
                }
            }
            return cursor;
        }

        char getCurrentChar() {
            String str = lines.get(line);
            if (column + offsetInLine < str.length()) {
                return str.charAt(column + offsetInLine);
            } else if (line < lines.size() - 1) {
                return '\n';
            } else {
                return 0;
            }
        }

        @SuppressWarnings("StatementWithEmptyBody")
        public void prevWord() {
            while (Character.isAlphabetic(getCurrentChar())
                    && moveLeft(1));
            while (!Character.isAlphabetic(getCurrentChar())
                    && moveLeft(1));
            while (Character.isAlphabetic(getCurrentChar())
                    && moveLeft(1));
            moveRight(1);
        }

        @SuppressWarnings("StatementWithEmptyBody")
        public void nextWord() {
            while (Character.isAlphabetic(getCurrentChar())
                    && moveRight(1));
            while (!Character.isAlphabetic(getCurrentChar())
                    && moveRight(1));
        }

        public void beginningOfLine() {
            column = offsetInLine = 0;
            wantedColumn = 0;
            ensureCursorVisible();
        }

        public void endOfLine() {
            int x = length(lines.get(line));
            moveRight(x, true);
        }

        public void prevPage() {
            int height = size.getRows() - computeHeader().size() - computeFooter().size();
            scrollUp(height - 2);
            column = 0;
            firstLineToDisplay = line;
            offsetInLineToDisplay = offsetInLine;
        }

        public void nextPage() {
            int height = size.getRows() - computeHeader().size() - computeFooter().size();
            scrollDown(height - 2);
            column = 0;
            firstLineToDisplay = line;
            offsetInLineToDisplay = offsetInLine;
        }

        public void scrollUp(int lines) {
            cursorUp(lines);
            moveDisplayUp(lines);
        }

        public void scrollDown(int lines) {
            cursorDown(lines);
            moveDisplayDown(lines);
        }

        public void firstLine() {
            line = 0;
            offsetInLine = column = 0;
            ensureCursorVisible();
        }

        public void lastLine() {
            line = lines.size() - 1;
            offsetInLine = column = 0;
            ensureCursorVisible();
        }

        boolean nextSearch() {
            boolean out = false;
            if (searchTerm == null) {
                setMessage("No current search pattern");
                return false;
            }
            setMessage(null);
            int cur = line;
            int dir = searchBackwards ? -1 : +1;
            int newPos = -1;
            int newLine = -1;
            // Search on current line
            List curRes = doSearch(lines.get(line));
            if (searchBackwards) {
                Collections.reverse(curRes);
            }
            for (int r : curRes) {
                if (searchBackwards ? r < offsetInLine + column : r > offsetInLine + column) {
                    newPos = r;
                    newLine = line;
                    break;
                }
            }
            // Check other lines
            if (newPos < 0) {
                while (true) {
                    cur = (cur + dir + lines.size()) % lines.size();
                    if (cur == line) {
                        break;
                    }
                    List res = doSearch(lines.get(cur));
                    if (!res.isEmpty()) {
                        newPos = searchBackwards ? res.get(res.size() - 1) : res.get(0);
                        newLine = cur;
                        break;
                    }
                }
            }
            if (newPos < 0) {
                if (!curRes.isEmpty()) {
                    newPos = curRes.get(0);
                    newLine = line;
                }
            }
            if (newPos >= 0) {
                if (newLine == line && newPos == offsetInLine + column) {
                    setMessage("This is the only occurence");
                    return false;
                }
                if ((searchBackwards && (newLine > line || (newLine == line && newPos > offsetInLine + column)))
                    || (!searchBackwards && (newLine < line || (newLine == line && newPos < offsetInLine + column)))) {
                    setMessage("Search Wrapped");
                }
                line = newLine;
                moveRight(newPos, true);
                out = true;
            } else {
                setMessage("\"" + searchTerm + "\" not found");
            }
            return out;
        }

        private List doSearch(String text) {
            Pattern pat = Pattern.compile(searchTerm,
                    (searchCaseSensitive ? 0 : Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE)
                            | (searchRegexp ? 0 : Pattern.LITERAL));
            Matcher m = pat.matcher(text);
            List res = new ArrayList<>();
            while (m.find()) {
                res.add(m.start());
                matchedLength = m.group(0).length();
            }
            return res;
        }

        protected int[] highlightStart() {
            int[] out = {-1, -1};
            if (mark) {
                out = getMarkStart();
            } else if (searchToReplace) {
                out[0] = line;
                out[1] = offsetInLine + column;
            }
            return out;
        }

        protected int[] highlightEnd() {
            int[] out = {-1, -1};
            if (mark) {
                out = getMarkEnd();
            } else if (searchToReplace && matchedLength > 0) {
                out[0] = line;
                int col = charPosition(offsetInLine + column) + matchedLength;
                if (col < lines.get(line).length()) {
                    out[1] = length(lines.get(line).substring(0, col));
                } else {
                    out[1] = length(lines.get(line));
                }
            }
             return out;
        }

        public void matching() {
            int opening = getCurrentChar();
            int idx = matchBrackets.indexOf(opening);
            if (idx >= 0) {
                int dir = (idx >= matchBrackets.length() / 2) ? -1 : +1;
                int closing = matchBrackets.charAt((idx + matchBrackets.length() / 2) % matchBrackets.length());

                int lvl = 1;
                int cur = line;
                int pos = offsetInLine + column;
                while (true) {
                    if ((pos + dir >= 0) && (pos + dir < getLine(cur).length())) {
                        pos += dir;
                    } else if ((cur + dir >= 0) && (cur + dir < lines.size())) {
                        cur += dir;
                        pos = dir > 0 ? 0 : lines.get(cur).length() - 1;
                        // Skip empty lines
                        if (pos < 0 || pos >= lines.get(cur).length()) {
                            continue;
                        }
                    } else {
                        setMessage("No matching bracket");
                        return;
                    }
                    int c = lines.get(cur).charAt(pos);
                    if (c == opening) {
                        lvl++;
                    } else if (c == closing) {
                        if (--lvl == 0) {
                            line = cur;
                            moveToChar(pos);
                            ensureCursorVisible();
                            return;
                        }
                    }
                }
            } else {
                setMessage("Not a bracket");
            }
        }

        private int length(String line) {
            return new AttributedStringBuilder().tabs(tabs).append(line).columnLength();
        }

        void copy() {
            if (uncut || cut2end || mark) {
                cutbuffer = new ArrayList<>();
            }
            if (mark) {
                int[] s = getMarkStart();
                int[] e = getMarkEnd();
                if (s[0] == e[0]) {
                    cutbuffer.add(lines.get(s[0]).substring(charPosition(s[0],s[1]), charPosition(e[0],e[1])));
                } else {
                    if (s[1] != 0) {
                        cutbuffer.add(lines.get(s[0]).substring(charPosition(s[0],s[1])));
                        s[0] = s[0] + 1;
                    }
                    for (int i = s[0]; i < e[0]; i++) {
                        cutbuffer.add(lines.get(i));
                    }
                    if (e[1] != 0) {
                        cutbuffer.add(lines.get(e[0]).substring(0, charPosition(e[0],e[1])));
                    }
                }
                mark = false;
                mark();
            } else if (cut2end) {
                String l = lines.get(line);
                int col = charPosition(offsetInLine + column);
                cutbuffer.add(l.substring(col));
                moveRight(l.substring(col).length());
            } else {
                cutbuffer.add(lines.get(line));
                cursorDown(1);
            }
            uncut = false;
        }

        void cut() {
            cut(false);
        }

        void cut(boolean toEnd) {
            if (lines.size() > 1) {
                if (uncut || cut2end || toEnd || mark) {
                    cutbuffer = new ArrayList<>();
                }
                if (mark) {
                    int[] s = getMarkStart();
                    int[] e = getMarkEnd();
                    if (s[0] == e[0]) {
                        String l = lines.get(s[0]);
                        int cols = charPosition(s[0], s[1]);
                        int cole = charPosition(e[0], e[1]);
                        cutbuffer.add(l.substring(cols, cole));
                        lines.set(s[0], l.substring(0, cols) + l.substring(cole));
                        computeAllOffsets();
                        moveRight(cols, true);
                    } else {
                        int ls = s[0];
                        int cs = charPosition(s[0], s[1]);
                        if (s[1] != 0) {
                            String l = lines.get(s[0]);
                            cutbuffer.add(l.substring(cs));
                            lines.set(s[0], l.substring(0, cs));
                            s[0] = s[0] + 1;
                        }
                        for (int i = s[0]; i < e[0]; i++) {
                            cutbuffer.add(lines.get(s[0]));
                            lines.remove(s[0]);
                        }
                        if (e[1] != 0) {
                            String l = lines.get(s[0]);
                            int col = charPosition(e[0], e[1]);
                            cutbuffer.add(l.substring(0, col));
                            lines.set(s[0], l.substring(col));
                        }
                        computeAllOffsets();
                        gotoLine(cs, ls);
                    }
                    mark = false;
                    mark();
                } else if (cut2end || toEnd) {
                    String l = lines.get(line);
                    int col = charPosition(offsetInLine + column);
                    cutbuffer.add(l.substring(col));
                    lines.set(line, l.substring(0, col));
                    if (toEnd) {
                        line++;
                        while (true) {
                            cutbuffer.add(lines.get(line));
                            lines.remove(line);
                            if (line > lines.size() - 1) {
                                line--;
                                break;
                            }
                        }
                    }
                } else {
                    cutbuffer.add(lines.get(line));
                    lines.remove(line);
                    offsetInLine = 0;
                    if (line > lines.size() - 1) {
                        line--;
                    }
                }
                display.clear();
                computeAllOffsets();
                dirty = true;
                uncut = false;
            }
        }

        void uncut() {
            if (cutbuffer.isEmpty()) {
                return;
            }
            String l = lines.get(line);
            int col = charPosition(offsetInLine + column);
            if (cut2end) {
                lines.set(line, l.substring(0, col) + cutbuffer.get(0) + l.substring(col));
                computeAllOffsets();
                moveRight(col + cutbuffer.get(0).length(), true);
            } else if (col == 0) {
                lines.addAll(line, cutbuffer);
                computeAllOffsets();
                if (cutbuffer.size() > 1) {
                    gotoLine(cutbuffer.get(cutbuffer.size() - 1).length(), line + cutbuffer.size());
                } else {
                    moveRight(cutbuffer.get(0).length(), true);
                }
            } else {
                int gotol = line;
                if (cutbuffer.size() == 1) {
                    lines.set(line, l.substring(0, col) + cutbuffer.get(0) + l.substring(col));
                } else {
                    lines.set(line++, l.substring(0, col) + cutbuffer.get(0));
                    gotol = line;
                    lines.add(line, cutbuffer.get(cutbuffer.size() - 1) + l.substring(col));
                    for (int i = cutbuffer.size() - 2; i > 0 ; i--) {
                        gotol++;
                        lines.add(line, cutbuffer.get(i));
                    }
                }
                computeAllOffsets();
                if (cutbuffer.size() > 1) {
                    gotoLine(cutbuffer.get(cutbuffer.size() - 1).length(), gotol);
                } else {
                    moveRight(col + cutbuffer.get(0).length(), true);
                }
            }
            display.clear();
            dirty = true;
            uncut = true;
        }

        void mark() {
            if (mark) {
                markPos[0] = line;
                markPos[1] = offsetInLine + column;
            } else {
                markPos[0] = -1;
                markPos[1] = -1;
            }
        }

        int[] getMarkStart() {
            int[] out = {-1, -1};
            if (!mark) {
                return out;
            }
            if (markPos[0] > line || (markPos[0] == line && markPos[1] > offsetInLine + column) ) {
                out[0] = line;
                out[1] = offsetInLine + column;
            } else {
                out = markPos;
            }
            return out;
        }

        int[] getMarkEnd() {
            int[] out = {-1, -1};
            if (!mark) {
                return out;
            }
            if (markPos[0] > line || (markPos[0] == line && markPos[1] > offsetInLine + column) ) {
                out = markPos;
            } else {
                out[0] = line;
                out[1] = offsetInLine + column;
            }
            return out;
        }

        void replaceFromCursor(int chars, String string) {
            int pos = charPosition(offsetInLine + column);
            String text = lines.get(line);
            String mod = text.substring(0, pos) + string;
            if (chars + pos < text.length()) {
                mod += text.substring(chars + pos);
            }
            lines.set(line, mod);
            dirty = true;
        }
    }

    /**
     *  Java implementation of nanorc highlighter
     *
     *  @author Matti Rinta-Nikkola
     */
    public static class SyntaxHighlighter {
        private final List rules = new ArrayList<>();
        private boolean startEndHighlight;
        private int ruleStartId = 0;

        private SyntaxHighlighter() {}

        protected static SyntaxHighlighter build(List syntaxFiles, String file, String syntaxName) {
            return build(syntaxFiles, file, syntaxName, false);
        }

        protected static SyntaxHighlighter build(List syntaxFiles, String file, String syntaxName
                , boolean ignoreErrors) {
            SyntaxHighlighter out = new SyntaxHighlighter();
            List defaultRules = new ArrayList<>();
            try {
                if (syntaxName == null || (syntaxName != null && !syntaxName.equals("none"))) {
                    for (Path p : syntaxFiles) {
                        try {
                            NanorcParser parser = new NanorcParser(p, syntaxName, file);
                            parser.parse();
                            if (parser.matches()) {
                                out.addRules(parser.getHighlightRules());
                                return out;
                            } else if (parser.isDefault()) {
                                defaultRules.addAll(parser.getHighlightRules());
                            }
                        } catch (IOException e) {
                            // ignore
                        }
                    }
                    out.addRules(defaultRules);
                }
            } catch (PatternSyntaxException e) {
                if (!ignoreErrors) {
                    throw e;
                }
            }
            return out;
        }

        /**
         * Build SyntaxHighlighter
         *
         * @param nanorc        Path of nano config file jnanorc
         * @param syntaxName    syntax name e.g 'Java'
         * @return              SyntaxHighlighter
         */
        public static SyntaxHighlighter build(Path nanorc, String syntaxName) {
            SyntaxHighlighter out = new SyntaxHighlighter();
            List syntaxFiles = new ArrayList<>();
            try {
                try (BufferedReader reader = new BufferedReader(new FileReader(nanorc.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)));
                                }
                            }
                        }
                        line = reader.readLine();
                    }
                }
                out = build(syntaxFiles, null, syntaxName);
            } catch (Exception e) {
                // ignore
            }
            return out;
        }

        /**
         * Build SyntaxHighlighter
         *
         * @param nanorcUrl     Url of nanorc file
         * @return              SyntaxHighlighter
         */
        public static SyntaxHighlighter build(String nanorcUrl) {
            SyntaxHighlighter out = new SyntaxHighlighter();
            InputStream inputStream;
            try {
                if (nanorcUrl.startsWith("classpath:")) {
                    inputStream = new Source.ResourceSource(nanorcUrl.substring(10), null).read();
                } else {
                    inputStream = new Source.URLSource(new URL(nanorcUrl), null).read();
                }
                NanorcParser parser = new NanorcParser(inputStream, null, null);
                parser.parse();
                out.addRules(parser.getHighlightRules());
            } catch (IOException e) {
                // ignore
            }
            return out;
        }

        private void addRules(List rules) {
            this.rules.addAll(rules);
        }

        public void reset() {
            ruleStartId = 0;
            startEndHighlight = false;
        }

        public AttributedString highlight(String string) {
            return highlight(new AttributedString(string));
        }

        public AttributedString highlight(AttributedStringBuilder asb) {
            return highlight(asb.toAttributedString());
        }

        public AttributedString highlight(AttributedString line) {
            if (rules.isEmpty()) {
                return line;
            }
            AttributedStringBuilder asb = new AttributedStringBuilder();
            asb.append(line);
            int startId = ruleStartId;
            boolean endHighlight = startEndHighlight;
            for (int i = startId; i < (endHighlight ? startId + 1 : rules.size()); i++) {
                HighlightRule rule = rules.get(i);
                switch (rule.getType()) {
                case PATTERN:
                    asb.styleMatches(rule.getPattern(), rule.getStyle());
                    break;
                case START_END:
                    boolean done = false;
                    Matcher start = rule.getStart().matcher(asb.toAttributedString());
                    Matcher end = rule.getEnd().matcher(asb.toAttributedString());
                    while (!done) {
                        AttributedStringBuilder a = new AttributedStringBuilder();
                        if (startEndHighlight && ruleStartId == i) {
                            if (end.find()) {
                                a.append(asb.columnSubSequence(0, end.end()), rule.getStyle());
                                a.append(asb.columnSubSequence(end.end(), asb.length()));
                                ruleStartId = 0;
                                startEndHighlight = false;
                            } else {
                                a.append(asb, rule.getStyle());
                                done = true;
                            }
                            asb = a;
                        } else {
                            if (start.find()) {
                                a.append(asb.columnSubSequence(0, start.start()));
                                if (end.find()) {
                                    a.append(asb.columnSubSequence(start.start(), end.end()), rule.getStyle());
                                    a.append(asb.columnSubSequence(end.end(), asb.length()));
                                } else {
                                    ruleStartId = i;
                                    startEndHighlight = true;
                                    a.append(asb.columnSubSequence(start.start(),asb.length()), rule.getStyle());
                                    done = true;
                                }
                                asb = a;
                            } else {
                                done = true;
                            }
                        }
                    }
                    break;
                }
            }
            return asb.toAttributedString();
        }

    }

    private static class HighlightRule {
        public enum RuleType {PATTERN, START_END}
        private final RuleType type;
        private Pattern pattern;
        private final AttributedStyle style;
        private Pattern start;
        private Pattern end;

        public HighlightRule(AttributedStyle style, Pattern pattern) {
             this.type = RuleType.PATTERN;
             this.pattern = pattern;
             this.style = style;
        }

        public HighlightRule(AttributedStyle style, Pattern start, Pattern end) {
             this.type = RuleType.START_END;
             this.style = style;
             this.start = start;
             this.end = end;
        }

        public RuleType getType() {
            return type;
        }

        public AttributedStyle getStyle() {
            return style;
        }

        public Pattern getPattern() {
            if (type == RuleType.START_END) {
                throw new IllegalAccessError();
            }
            return pattern;
        }

        public Pattern getStart() {
            if (type == RuleType.PATTERN) {
                throw new IllegalAccessError();
            }
            return start;
        }

        public Pattern getEnd() {
            if (type == RuleType.PATTERN) {
                throw new IllegalAccessError();
            }
            return end;
        }

        public static RuleType evalRuleType(List colorCfg) {
            RuleType out = null;
            if (colorCfg.get(0).equals("color") || colorCfg.get(0).equals("icolor")) {
                out = RuleType.PATTERN;
                if (colorCfg.size() == 4 && colorCfg.get(2).startsWith("start=") && colorCfg.get(3).startsWith("end=")) {
                    out = RuleType.START_END;
                }
            }
            return out;
        }

    }

    private static class NanorcParser {
        private static final String DEFAULT_SYNTAX = "default";
        private final String name;
        private final String target;
        private final List highlightRules = new ArrayList<>();
        private final BufferedReader reader;
        private boolean matches = false;
        private String syntaxName = "unknown";

        public NanorcParser(Path file, String name, String target) throws IOException {
            this(new Source.PathSource(file, null).read(), name, target);
        }

        public NanorcParser(InputStream in, String name, String target) {
            this.reader = new BufferedReader(new InputStreamReader(in));
            this.name = name;
            this.target = target;
        }

        public void parse() throws IOException {
            String line;
            int idx = 0;
            while ((line = reader.readLine()) != null) {
                idx++;
                line = line.trim();
                if (line.length() > 0 && !line.startsWith("#")) {
                    line = line.replaceAll("\\\\<", "\\\\b")
                            .replaceAll("\\\\>", "\\\\b")
                            .replaceAll("\\[:alnum:]", "\\\\p{Alnum}")
                            .replaceAll("\\[:alpha:]", "\\\\p{Alpha}")
                            .replaceAll("\\[:blank:]", "\\\\p{Blank}")
                            .replaceAll("\\[:cntrl:]", "\\\\p{Cntrl}")
                            .replaceAll("\\[:digit:]", "\\\\p{Digit}")
                            .replaceAll("\\[:graph:]", "\\\\p{Graph}")
                            .replaceAll("\\[:lower:]", "\\\\p{Lower}")
                            .replaceAll("\\[:print:]", "\\\\p{Print}")
                            .replaceAll("\\[:punct:]", "\\\\p{Punct}")
                            .replaceAll("\\[:space:]", "\\\\s")
                            .replaceAll("\\[:upper:]", "\\\\p{Upper}")
                            .replaceAll("\\[:xdigit:]", "\\\\p{XDigit}");
                    List parts = Parser.split(line);
                    if (parts.get(0).equals("syntax")) {
                        syntaxName = parts.get(1);
                        List filePatterns = new ArrayList<>();
                        if (name != null) {
                            if (name.equals(syntaxName)) {
                                matches = true;
                            } else {
                                break;
                            }
                        } else if (target != null) {
                            for (int i = 2; i < parts.size(); i++) {
                                filePatterns.add(Pattern.compile(parts.get(i)));
                            }
                            for (Pattern p: filePatterns) {
                                if (p.matcher(target).find()) {
                                    matches = true;
                                    break;
                                }
                            }
                            if (!matches && !syntaxName.equals(DEFAULT_SYNTAX)) {
                                break;
                            }
                        } else {
                            matches = true;
                        }
                    } else if (parts.get(0).equals("color")) {
                        addHighlightRule(syntaxName + idx, parts, false);
                    } else if (parts.get(0).equals("icolor")) {
                        addHighlightRule(syntaxName + idx, parts, true);
                    }
                }
            }
            reader.close();
        }

        public boolean matches() {
            return matches;
        }

        public List getHighlightRules() {
            return highlightRules;
        }

        public boolean isDefault() {
            return syntaxName.equals(DEFAULT_SYNTAX);
        }

        private void addHighlightRule(String reference, List parts, boolean caseInsensitive) {
            Map spec = new HashMap<>();
            spec.put(reference, parts.get(1));
            Styles.StyleCompiler sh = new Styles.StyleCompiler(spec, true);
            AttributedStyle style = new StyleResolver(sh::getStyle).resolve("." + reference);

            if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.PATTERN) {
                for (int i = 2; i < parts.size(); i++) {
                    highlightRules.add(new HighlightRule(style, doPattern(parts.get(i), caseInsensitive)));
                }
            } else if (HighlightRule.evalRuleType(parts) == HighlightRule.RuleType.START_END) {
                String s = parts.get(2);
                String e = parts.get(3);
                highlightRules.add(new HighlightRule(style
                                                   , doPattern(s.substring(7, s.length() - 1), caseInsensitive)
                                                   , doPattern(e.substring(5, e.length() - 1), caseInsensitive)));
            }
        }

        private Pattern doPattern(String regex, boolean caseInsensitive) {
            return caseInsensitive ? Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
                                   : Pattern.compile(regex);
        }

    }

    protected static class Parser {
        protected static List split(String s) {
            List out = new ArrayList<>();
            if (s.length() == 0) {
                return out;
            }
            int depth = 0;
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < s.length(); i++) {
                char c = s.charAt(i);
                if (c == '"') {
                    if (depth == 0) {
                        depth = 1;
                    } else {
                        char nextChar = i < s.length() - 1 ? s.charAt(i + 1) : ' ';
                        if (nextChar == ' ') {
                            depth = 0;
                        }
                    }
                } else if (c == ' ' && depth == 0 && sb.length() > 0) {
                    out.add(stripQuotes(sb.toString()));
                    sb = new StringBuilder();
                    continue;
                }
                if (sb.length() > 0 || (c != ' ' && c != '\t')) {
                    sb.append(c);
                }
            }
            if (sb.length() > 0) {
                out.add(stripQuotes(sb.toString()));
            }
            return out;
        }

        private static String stripQuotes(String s) {
            String out = s.trim();
            if (s.startsWith("\"") && s.endsWith("\"")) {
                out = s.substring(1, s.length() - 1);
            }
            return out;
        }
    }

    protected static class PatternHistory {
        private final Path historyFile;
        private final int size = 100;
        private List patterns = new ArrayList<>();
        private int patternId = -1;
        private boolean lastMoveUp = false;

        public PatternHistory(Path historyFile) {
            this.historyFile = historyFile;
            load();
        }

        public String up(String hint) {
            String out = hint;
            if (patterns.size() > 0 && patternId < patterns.size()) {
                if (!lastMoveUp && patternId > 0 && patternId < patterns.size() - 1) {
                    patternId++;
                }
                if (patternId < 0) {
                    patternId = 0;
                }
                boolean found = false;
                for (int pid = patternId; pid < patterns.size(); pid++) {
                    if (hint.length() == 0
                            || patterns.get(pid).startsWith(hint)) {
                        patternId = pid + 1;
                        out = patterns.get(pid);
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    patternId = patterns.size();
                }
            }
            lastMoveUp = true;
            return out;
        }

        public String down(String hint) {
            String out = hint;
            if (patterns.size() > 0) {
                if (lastMoveUp) {
                    patternId--;
                }
                if (patternId < 0) {
                    patternId = -1;
                } else {
                    boolean found = false;
                    for (int pid = patternId;  pid >= 0; pid--) {
                        if (hint.length() == 0 || patterns.get(pid).startsWith(hint)) {
                            patternId = pid - 1;
                            out = patterns.get(pid);
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        patternId = -1;
                    }
                }
            }
            lastMoveUp = false;
            return out;
        }

        public void add(String pattern) {
            if (pattern.trim().length() == 0) {
                return;
            }
            patterns.remove(pattern);
            if (patterns.size() > size) {
                patterns.remove(patterns.size() - 1);
            }
            patterns.add(0, pattern);
            patternId = -1;
        }

        public void persist() {
            if (historyFile == null) {
                return;
            }
            try {
                try (BufferedWriter writer = Files.newBufferedWriter(
                        historyFile.toAbsolutePath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
                    for (String s : patterns) {
                        if (s.trim().length() > 0) {
                            writer.append(s);
                            writer.newLine();
                        }
                    }
                }
            } catch (Exception e) {
                // ignore
            }
        }

        private void load() {
            if (historyFile == null) {
                return;
            }
            try {
                if (Files.exists(historyFile)) {
                    patterns = new ArrayList<>();
                    try (BufferedReader reader = Files
                            .newBufferedReader(historyFile)) {
                        reader.lines().forEach(line -> patterns.add(line));
                    }
                }
            } catch (Exception e) {
                // ignore
            }
        }

    }

    public Nano(Terminal terminal, File root) {
        this(terminal, root.toPath());
    }

    public Nano(Terminal terminal, Path root) {
        this(terminal, root, null);
    }

    public Nano(Terminal terminal, Path root, Options opts) {
        this(terminal, root, opts, null);
    }

    public Nano(Terminal terminal, Path root, Options opts, ConfigurationPath configPath) {
        this.terminal = terminal;
        this.windowsTerminal = terminal.getClass().getSimpleName().endsWith("WinSysTerminal");
        this.root = root;
        this.display = new Display(terminal, true);
        this.bindingReader = new BindingReader(terminal.reader());
        this.size = new Size();
        Attributes attrs = terminal.getAttributes();
        this.vsusp = attrs.getControlChar(ControlChar.VSUSP);
        if (vsusp > 0) {
            attrs.setControlChar(ControlChar.VSUSP, 0);
            terminal.setAttributes(attrs);
        }
        Path nanorc = configPath != null ? configPath.getConfig("jnanorc") : null;
        boolean ignorercfiles = opts != null && opts.isSet("ignorercfiles");
        if (nanorc != null && !ignorercfiles) {
            try {
                parseConfig(nanorc);
            } catch (IOException e) {
                errorMessage = "Encountered error while reading config file: " + nanorc;
            }
        } 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) {
            this.restricted = opts.isSet("restricted");
            this.syntaxName = null;
            if (opts.isSet("syntax")) {
                this.syntaxName = opts.get("syntax");
                nanorcIgnoreErrors = false;
            }
            if (opts.isSet("backup")) {
                writeBackup = true;
            }
            if (opts.isSet("quotestr")) {
                quoteStr = opts.get("quotestr");
            }
            if (opts.isSet("tabsize")) {
                tabs = opts.getNumber("tabsize");
            }
            if (opts.isSet("quickblank")) {
                quickBlank = true;
            }
            if (opts.isSet("constantshow")) {
                constantCursor = true;
            }
            if (opts.isSet("emptyline")) {
                oneMoreLine = false;
            }
            if (opts.isSet("jumpyscrolling")) {
                smoothScrolling = false;
            }
            if (opts.isSet("linenumbers")) {
                printLineNumbers = true;
            }
            if (opts.isSet("mouse")) {
                mouseSupport = true;
            }
            if (opts.isSet("softwrap")) {
                wrapping = true;
            }
            if (opts.isSet("atblanks")) {
                atBlanks = true;
            }
            if (opts.isSet("suspend")) {
                enableSuspension();
            }
            if (opts.isSet("view")) {
                view = true;
            }
            if (opts.isSet("cutfromcursor")) {
                cut2end = true;
            }
            if (opts.isSet("tempfile")) {
                tempFile = true;
            }
            if (opts.isSet("historylog")) {
                historyLog = opts.get("historyLog");
            }
            if (opts.isSet("tabstospaces")) {
                tabsToSpaces = true;
            }
            if (opts.isSet("autoindent")) {
                autoIndent = true;
            }
        }
        bindKeys();
        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("linenumbers")) {
                            printLineNumbers = val;
                        } else if (option.equals("jumpyscrolling")) {
                            smoothScrolling = !val;
                        } else if (option.equals("smooth")) {
                            smoothScrolling = val;
                        } else if (option.equals("softwrap")) {
                            wrapping = val;
                        } else if (option.equals("mouse")) {
                            mouseSupport = val;
                        } else if (option.equals("emptyline")) {
                            oneMoreLine = val;
                        } else if (option.equals("morespace")) {
                            oneMoreLine = !val;
                        } else if (option.equals("constantshow")) {
                            constantCursor = val;
                        } else if (option.equals("quickblank")) {
                            quickBlank = val;
                        } else if (option.equals("atblanks")) {
                            atBlanks = val;
                        } else if (option.equals("suspend")) {
                            enableSuspension();
                        } else if (option.equals("view")) {
                            view = val;
                        } else if (option.equals("cutfromcursor")) {
                            cut2end = val;
                        } else if (option.equals("tempfile")) {
                            tempFile = val;
                        } else if (option.equals("tabstospaces")) {
                            tabsToSpaces = val;
                        } else if (option.equals("autoindent")) {
                            autoIndent = val;
                        } else {
                            errorMessage = "Nano 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("quotestr")) {
                            quoteStr = val;
                        } else if (option.equals("punct")) {
                            punct = val;
                        } else if (option.equals("matchbrackets")) {
                            matchBrackets = val;
                        } else if (option.equals("brackets")) {
                            brackets = val;
                        } else if (option.equals("historylog")) {
                            historyLog = val;
                        } else {
                            errorMessage = "Nano config: Unknown or unsupported configuration option " + option;
                        }
                    } else if (parts.get(0).equals("bind") || parts.get(0).equals("unbind")) {
                        errorMessage = "Nano config: Key bindings can not be changed!";
                    } else {
                        errorMessage = "Nano config: Bad configuration '" + line + "'";
                    }
                }
                line = reader.readLine();
            }
        }
    }

    public void setRestricted(boolean restricted) {
        this.restricted = restricted;
    }

    public void open(String... files) throws IOException {
        open(Arrays.asList(files));
    }

    public void open(List files) throws IOException {
        for (String file : files) {
            file = file.startsWith("~") ? file.replace("~", System.getProperty("user.home")) : file;
            if (file.contains("*") || file.contains("?")) {
                for (Path p: Commands.findFiles(root, file)) {
                    buffers.add(new Buffer(p.toString()));
                }
            } else {
                buffers.add(new Buffer(file));
            }
        }
    }

    public void run() throws IOException {
        if (buffers.isEmpty()) {
            buffers.add(new Buffer(null));
        }
        buffer = buffers.get(bufferIndex);

        Attributes attributes = terminal.getAttributes();
        Attributes newAttr = new Attributes(attributes);
        if (vsusp > 0) {
            attributes.setControlChar(ControlChar.VSUSP, vsusp);
        }
        newAttr.setLocalFlags(EnumSet.of(LocalFlag.ICANON, LocalFlag.ECHO, LocalFlag.IEXTEN), false);
        newAttr.setInputFlags(EnumSet.of(InputFlag.IXON, InputFlag.ICRNL, InputFlag.INLCR), false);
        newAttr.setControlChar(ControlChar.VMIN, 1);
        newAttr.setControlChar(ControlChar.VTIME, 0);
        newAttr.setControlChar(ControlChar.VINTR, 0);
        terminal.setAttributes(newAttr);
        terminal.puts(Capability.enter_ca_mode);
        terminal.puts(Capability.keypad_xmit);
        if (mouseSupport) {
            terminal.trackMouse(Terminal.MouseTracking.Normal);
        }

        this.shortcuts = standardShortcuts();

        SignalHandler prevHandler = null;
        Status status = Status.getStatus(terminal, false);
        try {
            size.copy(terminal.getSize());
            if (status != null) {
                status.suspend();
            }
            buffer.open();
            if (errorMessage != null) {
                setMessage(errorMessage);
                errorMessage = null;
            } else if (buffer.file != null) {
                setMessage("Read " + buffer.lines.size() + " lines");
            }

            display.clear();
            display.reset();
            display.resize(size.getRows(), size.getColumns());
            prevHandler = terminal.handle(Signal.WINCH, this::handle);

            display();

            while (true) {
                Operation op;
                switch (op = readOperation(keys)) {
                    case QUIT:
                        if (quit()) {
                            return;
                        }
                        break;
                    case WRITE:
                        write();
                        break;
                    case READ:
                        read();
                        break;
                    case UP:
                        buffer.moveUp(1);
                        break;
                    case DOWN:
                        buffer.moveDown(1);
                        break;
                    case LEFT:
                        buffer.moveLeft(1);
                        break;
                    case RIGHT:
                        buffer.moveRight(1);
                        break;
                    case INSERT:
                        buffer.insert(bindingReader.getLastBinding());
                        break;
                    case BACKSPACE:
                        buffer.backspace(1);
                        break;
                    case DELETE:
                        buffer.delete(1);
                        break;
                    case WRAP:
                        wrap();
                        break;
                    case NUMBERS:
                        numbers();
                        break;
                    case SMOOTH_SCROLLING:
                        smoothScrolling();
                        break;
                    case MOUSE_SUPPORT:
                        mouseSupport();
                        break;
                    case ONE_MORE_LINE:
                        oneMoreLine();
                        break;
                    case CLEAR_SCREEN:
                        clearScreen();
                        break;
                    case PREV_BUFFER:
                        prevBuffer();
                        break;
                    case NEXT_BUFFER:
                        nextBuffer();
                        break;
                    case CUR_POS:
                        curPos();
                        break;
                    case PREV_WORD:
                        buffer.prevWord();
                        break;
                    case NEXT_WORD:
                        buffer.nextWord();
                        break;
                    case BEGINNING_OF_LINE:
                        buffer.beginningOfLine();
                        break;
                    case END_OF_LINE:
                        buffer.endOfLine();
                        break;
                    case FIRST_LINE:
                        buffer.firstLine();
                        break;
                    case LAST_LINE:
                        buffer.lastLine();
                        break;
                    case PREV_PAGE:
                        buffer.prevPage();
                        break;
                    case NEXT_PAGE:
                        buffer.nextPage();
                        break;
                    case SCROLL_UP:
                        buffer.scrollUp(1);
                        break;
                    case SCROLL_DOWN:
                        buffer.scrollDown(1);
                        break;
                    case SEARCH:
                        searchToReplace = false;
                        searchAndReplace();
                        break;
                    case REPLACE:
                        searchToReplace = true;
                        searchAndReplace();
                        break;
                    case NEXT_SEARCH:
                        buffer.nextSearch();
                        break;
                    case HELP:
                        help("nano-main-help.txt");
                        break;
                    case CONSTANT_CURSOR:
                        constantCursor();
                        break;
                    case VERBATIM:
                        buffer.insert(new String(Character.toChars(bindingReader.readCharacter())));
                        break;
                    case MATCHING:
                        buffer.matching();
                        break;
                    case MOUSE_EVENT:
                        mouseEvent();
                        break;
                    case TOGGLE_SUSPENSION:
                        toggleSuspension();
                        break;
                    case COPY:
                        buffer.copy();
                        break;
                    case CUT:
                        buffer.cut();
                        break;
                    case UNCUT:
                        buffer.uncut();
                        break;
                    case GOTO:
                        gotoLine();
                        curPos();
                        break;
                    case CUT_TO_END_TOGGLE:
                        cut2end = !cut2end;
                        setMessage("Cut to end " + (cut2end ? "enabled" : "disabled"));
                        break;
                    case CUT_TO_END:
                        buffer.cut(true);
                        break;
                    case MARK:
                        mark = !mark;
                        setMessage("Mark " + (mark ? "Set" : "Unset"));
                        buffer.mark();
                        break;
                    case HIGHLIGHT:
                        highlight = !highlight;
                        setMessage("Highlight " + (highlight ? "enabled" : "disabled"));
                        break;
                    case TABS_TO_SPACE:
                        tabsToSpaces = !tabsToSpaces;
                        setMessage("Conversion of typed tabs to spaces " + (tabsToSpaces ? "enabled" : "disabled"));
                        break;
                    case AUTO_INDENT:
                        autoIndent = !autoIndent;
                        setMessage("Auto indent " + (autoIndent ? "enabled" : "disabled"));
                        break;
                    default:
                        setMessage("Unsupported " + op.name().toLowerCase().replace('_', '-'));
                        break;
                }
                display();
            }
        } finally {
            if (mouseSupport) {
                terminal.trackMouse(Terminal.MouseTracking.Off);
            }
            if (!terminal.puts(Capability.exit_ca_mode)) {
                terminal.puts(Capability.clear_screen);
            }
            terminal.puts(Capability.keypad_local);
            terminal.flush();
            terminal.setAttributes(attributes);
            terminal.handle(Signal.WINCH, prevHandler);
            if (status != null) {
                status.restore();
            }
            patternHistory.persist();
       }
    }

    private int editInputBuffer(Operation operation, int curPos) {
        switch (operation) {
        case INSERT:
            editBuffer.insert(curPos++, bindingReader.getLastBinding());
            break;
        case BACKSPACE:
            if (curPos > 0) {
                editBuffer.deleteCharAt(--curPos);
            }
            break;
        case LEFT:
            if (curPos > 0) {
                curPos--;
            }
            break;
        case RIGHT:
            if (curPos < editBuffer.length()) {
                curPos++;
            }
            break;
        }
        return curPos;
    }

    boolean write() throws IOException {
        KeyMap writeKeyMap = new KeyMap<>();
        if (!restricted) {
            writeKeyMap.setUnicode(Operation.INSERT);
            for (char i = 32; i < 256; i++) {
                writeKeyMap.bind(Operation.INSERT, Character.toString(i));
            }
            for (char i = 'A'; i <= 'Z'; i++) {
                writeKeyMap.bind(Operation.DO_LOWER_CASE, alt(i));
            }
            writeKeyMap.bind(Operation.BACKSPACE, del());
            writeKeyMap.bind(Operation.APPEND_MODE, alt('a'));
            writeKeyMap.bind(Operation.PREPEND_MODE, alt('p'));
            writeKeyMap.bind(Operation.BACKUP, alt('b'));
            writeKeyMap.bind(Operation.TO_FILES, ctrl('T'));
        }
        writeKeyMap.bind(Operation.MAC_FORMAT, alt('m'));
        writeKeyMap.bind(Operation.DOS_FORMAT, alt('d'));
        writeKeyMap.bind(Operation.ACCEPT, "\r");
        writeKeyMap.bind(Operation.CANCEL, ctrl('C'));
        writeKeyMap.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1));
        writeKeyMap.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse));
        writeKeyMap.bind(Operation.TOGGLE_SUSPENSION, alt('z'));
        writeKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right));
        writeKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left));

        editMessage = getWriteMessage();
        editBuffer.setLength(0);
        editBuffer.append(buffer.file == null ? "" : buffer.file);
        int curPos = editBuffer.length();
        this.shortcuts = writeShortcuts();
        display(curPos);
        while (true) {
            Operation op = readOperation(writeKeyMap);
            switch (op) {
                case CANCEL:
                    editMessage = null;
                    this.shortcuts = standardShortcuts();
                    return false;
                case ACCEPT:
                    editMessage = null;
                    if (save(editBuffer.toString())) {
                        this.shortcuts = standardShortcuts();
                        return true;
                    }
                    return false;
                case HELP:
                    help("nano-write-help.txt");
                    break;
                case MAC_FORMAT:
                    buffer.format = (buffer.format == WriteFormat.MAC) ? WriteFormat.UNIX : WriteFormat.MAC;
                    break;
                case DOS_FORMAT:
                    buffer.format = (buffer.format == WriteFormat.DOS) ? WriteFormat.UNIX : WriteFormat.DOS;
                    break;
                case APPEND_MODE:
                    writeMode = (writeMode == WriteMode.APPEND) ? WriteMode.WRITE : WriteMode.APPEND;
                    break;
                case PREPEND_MODE:
                    writeMode = (writeMode == WriteMode.PREPEND) ? WriteMode.WRITE : WriteMode.PREPEND;
                    break;
                case BACKUP:
                    writeBackup = !writeBackup;
                    break;
                case MOUSE_EVENT:
                    mouseEvent();
                    break;
                case TOGGLE_SUSPENSION:
                    toggleSuspension();
                    break;
                default:
                    curPos = editInputBuffer(op, curPos);
                    break;
            }
            editMessage = getWriteMessage();
            display(curPos);
        }
    }

    private Operation readOperation(KeyMap keymap) {
        while (true) {
            Operation op = bindingReader.readBinding(keymap);
            if (op == Operation.DO_LOWER_CASE) {
                bindingReader.runMacro(bindingReader.getLastBinding().toLowerCase());
            } else {
                return op;
            }
        }
    }

    private boolean save(String name) throws IOException {
        Path orgPath = buffer.file != null ? root.resolve(buffer.file) : null;
        Path newPath = root.resolve(name);
        boolean isSame = orgPath != null && Files.exists(orgPath) && Files.exists(newPath) && Files.isSameFile(orgPath, newPath);
        if (!isSame && Files.exists(Paths.get(name)) && writeMode == WriteMode.WRITE) {
            Operation op = getYNC("File exists, OVERWRITE ? ");
            if (op != Operation.YES) {
                return false;
            }
        } else if (!Files.exists(newPath)) {
            newPath.toFile().createNewFile();
        }
        Path t = Files.createTempFile("jline-", ".temp");
        try (OutputStream os = Files.newOutputStream(t, StandardOpenOption.WRITE,
                                                        StandardOpenOption.TRUNCATE_EXISTING,
                                                        StandardOpenOption.CREATE)) {
            if (writeMode == WriteMode.APPEND) {
                if (Files.isReadable(newPath)) {
                    Files.copy(newPath, os);
                }
            }
            Writer w = new OutputStreamWriter(os, buffer.charset);
            for (int i = 0; i < buffer.lines.size(); i++) {
                w.write(buffer.lines.get(i));
                switch (buffer.format) {
                    case UNIX:
                        w.write("\n");
                        break;
                    case DOS:
                        w.write("\r\n");
                        break;
                    case MAC:
                        w.write("\r");
                        break;
                }
            }
            w.flush();
            if (writeMode == WriteMode.PREPEND) {
                if (Files.isReadable(newPath)) {
                    Files.copy(newPath, os);
                }
            }
            if (writeBackup) {
                Files.move(newPath, newPath.resolveSibling(newPath.getFileName().toString() + "~"), StandardCopyOption.REPLACE_EXISTING);
            }
            Files.move(t, newPath, StandardCopyOption.REPLACE_EXISTING);
            if (writeMode == WriteMode.WRITE) {
                buffer.file = name;
                buffer.dirty = false;
            }
            setMessage("Wrote " + buffer.lines.size() + " lines");
            return true;
        } catch (IOException e) {
            setMessage("Error writing " + name + ": " + e.toString());
            return false;
        } finally {
            Files.deleteIfExists(t);
            writeMode = WriteMode.WRITE;
        }
    }

    private Operation getYNC(String message) {
        return getYNC(message, false);
    }

    private Operation getYNC(String message, boolean andAll) {
        String oldEditMessage = editMessage;
        String oldEditBuffer = editBuffer.toString();
        LinkedHashMap oldShortcuts = shortcuts;
        try {
            editMessage = message;
            editBuffer.setLength(0);
            KeyMap yncKeyMap = new KeyMap<>();
            yncKeyMap.bind(Operation.YES, "y", "Y");
            if (andAll) {
                yncKeyMap.bind(Operation.ALL, "a", "A");
            }
            yncKeyMap.bind(Operation.NO, "n", "N");
            yncKeyMap.bind(Operation.CANCEL, ctrl('C'));
            shortcuts = new LinkedHashMap<>();
            shortcuts.put(" Y", "Yes");
            if (andAll) {
                shortcuts.put(" A", "All");
            }
            shortcuts.put(" N", "No");
            shortcuts.put("^C", "Cancel");
            display();
            return readOperation(yncKeyMap);
        } finally {
            editMessage = oldEditMessage;
            editBuffer.append(oldEditBuffer);
            shortcuts = oldShortcuts;
        }
    }

    private String getWriteMessage() {
        StringBuilder sb = new StringBuilder();
        sb.append("File Name to ");
        switch (writeMode) {
            case WRITE:
                sb.append("Write");
                break;
            case APPEND:
                sb.append("Append");
                break;
            case PREPEND:
                sb.append("Prepend");
                break;
        }
        switch (buffer.format) {
            case UNIX:
                break;
            case DOS:
                sb.append(" [DOS Format]");
                break;
            case MAC:
                sb.append(" [Mac Format]");
                break;
        }
        if (writeBackup) {
            sb.append(" [Backup]");
        }
        sb.append(": ");
        return sb.toString();
    }

    void read() {
        KeyMap readKeyMap = new KeyMap<>();
        readKeyMap.setUnicode(Operation.INSERT);
        for (char i = 32; i < 256; i++) {
            readKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        for (char i = 'A'; i <= 'Z'; i++) {
            readKeyMap.bind(Operation.DO_LOWER_CASE, alt(i));
        }
        readKeyMap.bind(Operation.BACKSPACE, del());
        readKeyMap.bind(Operation.NEW_BUFFER, alt('f'));
        readKeyMap.bind(Operation.TO_FILES, ctrl('T'));
        readKeyMap.bind(Operation.EXECUTE, ctrl('X'));
        readKeyMap.bind(Operation.ACCEPT, "\r");
        readKeyMap.bind(Operation.CANCEL, ctrl('C'));
        readKeyMap.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1));
        readKeyMap.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse));
        readKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right));
        readKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left));

        editMessage = getReadMessage();
        editBuffer.setLength(0);
        int curPos = editBuffer.length();
        this.shortcuts = readShortcuts();
        display(curPos);
        while (true) {
            Operation op = readOperation(readKeyMap);
            switch (op) {
                case CANCEL:
                    editMessage = null;
                    this.shortcuts = standardShortcuts();
                    return;
                case ACCEPT:
                    editMessage = null;
                    String file = editBuffer.toString();
                    boolean empty = file.isEmpty();
                    Path p = empty ? null : root.resolve(file);
                    if (!readNewBuffer && !empty && !Files.exists(p)) {
                        setMessage("\"" + file + "\" not found");
                    } else if (!empty && Files.isDirectory(p)) {
                        setMessage("\"" + file + "\" is a directory");
                    } else if (!empty && !Files.isRegularFile(p)) {
                        setMessage("\"" + file + "\" is not a regular file");
                    } else {
                        Buffer buf = new Buffer(empty ? null : file);
                        try {
                            buf.open();
                            if (readNewBuffer) {
                                buffers.add(++bufferIndex, buf);
                                buffer = buf;
                            } else {
                                buffer.insert(String.join("\n", buf.lines));
                            }
                            setMessage(null);
                        } catch (IOException e) {
                            setMessage("Error reading " + file + ": " + e.getMessage());
                        }
                    }
                    this.shortcuts = standardShortcuts();
                    return;
                case HELP:
                    help("nano-read-help.txt");
                    break;
                case NEW_BUFFER:
                    readNewBuffer = !readNewBuffer;
                    break;
                case MOUSE_EVENT:
                    mouseEvent();
                    break;
                default:
                    curPos = editInputBuffer(op, curPos);
                    break;
            }
            editMessage = getReadMessage();
            display(curPos);
        }
    }

    private String getReadMessage() {
        StringBuilder sb = new StringBuilder();
        sb.append("File to insert");
        if (readNewBuffer) {
            sb.append(" into new buffer");
        }
        sb.append(" [from ./]: ");
        return sb.toString();
    }

    void gotoLine() throws IOException {
        KeyMap readKeyMap = new KeyMap<>();
        readKeyMap.setUnicode(Operation.INSERT);
        for (char i = 32; i < 256; i++) {
            readKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        readKeyMap.bind(Operation.BACKSPACE, del());
        readKeyMap.bind(Operation.ACCEPT, "\r");
        readKeyMap.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1));
        readKeyMap.bind(Operation.CANCEL, ctrl('C'));
        readKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right));
        readKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left));
        readKeyMap.bind(Operation.FIRST_LINE, ctrl('Y'));
        readKeyMap.bind(Operation.LAST_LINE, ctrl('V'));
        readKeyMap.bind(Operation.SEARCH, ctrl('T'));

        editMessage = "Enter line number, column number: ";
        editBuffer.setLength(0);
        int curPos = editBuffer.length();
        this.shortcuts = gotoShortcuts();
        display(curPos);
        while (true) {
            Operation op = readOperation(readKeyMap);
            switch (op) {
                case CANCEL:
                    editMessage = null;
                    this.shortcuts = standardShortcuts();
                    return;
                case FIRST_LINE:
                    editMessage = null;
                    buffer.firstLine();
                    this.shortcuts = standardShortcuts();
                    return;
                case LAST_LINE:
                    editMessage = null;
                    buffer.lastLine();
                    this.shortcuts = standardShortcuts();
                    return;
                case SEARCH:
                    searchToReplace = false;
                    searchAndReplace();
                    return;
                case ACCEPT:
                    editMessage = null;
                    String[] pos = editBuffer.toString().split(",", 2);
                    int[] args = { 0, 0 };
                    try {
                        for (int i = 0; i < pos.length; i++) {
                            if (pos[i].trim().length() > 0) {
                                args[i] = Integer.parseInt(pos[i]) - 1;
                                if (args[i] < 0) {
                                    throw new NumberFormatException();
                                }
                            }
                        }
                        buffer.gotoLine(args[1], args[0]);
                    } catch (NumberFormatException ex) {
                        setMessage("Invalid line or column number");
                    } catch (Exception ex) {
                        setMessage("Internal error: " + ex.getMessage());
                    }
                    this.shortcuts = standardShortcuts();
                    return;
                case HELP:
                    help("nano-goto-help.txt");
                    break;
                default:
                    curPos = editInputBuffer(op, curPos);
                    break;
            }
            display(curPos);
        }
    }

    private LinkedHashMap gotoShortcuts() {
        LinkedHashMap shortcuts = new LinkedHashMap<>();
        shortcuts.put("^G", "Get Help");
        shortcuts.put("^Y", "First Line");
        shortcuts.put("^T", "Go To Text");
        shortcuts.put("^C", "Cancel");
        shortcuts.put("^V", "Last Line");
        return shortcuts;
    }

    private LinkedHashMap readShortcuts() {
        LinkedHashMap shortcuts = new LinkedHashMap<>();
        shortcuts.put("^G", "Get Help");
        shortcuts.put("^T", "To Files");
        shortcuts.put("M-F", "New Buffer");
        shortcuts.put("^C", "Cancel");
        shortcuts.put("^X", "Execute Command");
        return shortcuts;
    }

    private LinkedHashMap writeShortcuts() {
        LinkedHashMap s = new LinkedHashMap<>();
        s.put("^G", "Get Help");
        s.put("M-M", "Mac Format");
        s.put("^C", "Cancel");
        s.put("M-D", "DOS Format");
        if (!restricted) {
            s.put("^T", "To Files");
            s.put("M-P", "Prepend");
            s.put("M-A", "Append");
            s.put("M-B", "Backup File");
        }
        return s;
    }

    private LinkedHashMap helpShortcuts() {
        LinkedHashMap s = new LinkedHashMap<>();
        s.put("^L", "Refresh");
        s.put("^Y", "Prev Page");
        s.put("^P", "Prev Line");
        s.put("M-\\", "First Line");
        s.put("^X", "Exit");
        s.put("^V", "Next Page");
        s.put("^N", "Next Line");
        s.put("M-/", "Last Line");
        return s;
    }

    private LinkedHashMap searchShortcuts() {
        LinkedHashMap s = new LinkedHashMap<>();
        s.put("^G", "Get Help");
        s.put("^Y", "First Line");
        if (searchToReplace) {
            s.put("^R", "No Replace");
        } else {
            s.put("^R", "Replace");
            s.put("^W", "Beg of Par");
        }
        s.put("M-C", "Case Sens");
        s.put("M-R", "Regexp");
        s.put("^C", "Cancel");
        s.put("^V", "Last Line");
        s.put("^T", "Go To Line");
        if (!searchToReplace) {
            s.put("^O", "End of Par");
        }
        s.put("M-B", "Backwards");
        s.put("^P", "PrevHstory");
        return s;
    }

    private LinkedHashMap replaceShortcuts() {
        LinkedHashMap s = new LinkedHashMap<>();
        s.put("^G", "Get Help");
        s.put("^Y", "First Line");
        s.put("^P", "PrevHstory");
        s.put("^C", "Cancel");
        s.put("^V", "Last Line");
        s.put("^N", "NextHstory");
        return s;
    }

    private LinkedHashMap standardShortcuts() {
        LinkedHashMap s = new LinkedHashMap<>();
        s.put("^G", "Get Help");
        if (!view) {
            s.put("^O", "WriteOut");
        }
        s.put("^R", "Read File");
        s.put("^Y", "Prev Page");
        if (!view) {
            s.put("^K", "Cut Text");
        }
        s.put("^C", "Cur Pos");
        s.put("^X", "Exit");
        if (!view) {
            s.put("^J", "Justify");
        }
        s.put("^W", "Where Is");
        s.put("^V", "Next Page");
        if (!view) {
            s.put("^U", "UnCut Text");
        }
        s.put("^T", "To Spell");
        return s;
    }

    void help(String help) {
        Buffer org = this.buffer;
        Buffer newBuf = new Buffer(null);
        try (InputStream is = getClass().getResourceAsStream(help)) {
            newBuf.open(is);
        } catch (IOException e) {
            setMessage("Unable to read help");
            return;
        }
        LinkedHashMap oldShortcuts = this.shortcuts;
        this.shortcuts = helpShortcuts();
        boolean oldWrapping = this.wrapping;
        boolean oldPrintLineNumbers = this.printLineNumbers;
        boolean oldConstantCursor = this.constantCursor;
        boolean oldAtBlanks = this.atBlanks;
        boolean oldHighlight = this.highlight;
        String oldEditMessage = this.editMessage;
        this.editMessage = "";
        this.wrapping = true;
        this.atBlanks = true;
        this.printLineNumbers = false;
        this.constantCursor = false;
        this.highlight = false;
        this.buffer = newBuf;
        if (!oldWrapping) {
            buffer.computeAllOffsets();
        }
        try {
            this.message = null;
            terminal.puts(Capability.cursor_invisible);
            display();
            while (true) {
                switch (readOperation(keys)) {
                    case QUIT:
                        return;
                    case FIRST_LINE:
                        buffer.firstLine();
                        break;
                    case LAST_LINE:
                        buffer.lastLine();
                        break;
                    case PREV_PAGE:
                        buffer.prevPage();
                        break;
                    case NEXT_PAGE:
                        buffer.nextPage();
                        break;
                    case UP:
                        buffer.scrollUp(1);
                        break;
                    case DOWN:
                        buffer.scrollDown(1);
                        break;
                    case CLEAR_SCREEN:
                        clearScreen();
                        break;
                    case MOUSE_EVENT:
                        mouseEvent();
                        break;
                    case TOGGLE_SUSPENSION:
                        toggleSuspension();
                        break;
               }
                display();
            }
        } finally {
            this.buffer = org;
            this.wrapping = oldWrapping;
            this.printLineNumbers = oldPrintLineNumbers;
            this.constantCursor = oldConstantCursor;
            this.shortcuts = oldShortcuts;
            this.atBlanks = oldAtBlanks;
            this.highlight = oldHighlight;
            this.editMessage = oldEditMessage;
            terminal.puts(Capability.cursor_visible);
            if (!oldWrapping) {
                buffer.computeAllOffsets();
            }
        }
    }

    void searchAndReplace() {
        try {
            search();
            if (!searchToReplace) {
                return;
            }
            String replaceTerm = replace();
            int replaced = 0;
            boolean all = false;
            boolean found = true;
            List matches = new ArrayList<>();
            Operation op = Operation.NO;
            while (found) {
                found = buffer.nextSearch();
                if (found) {
                    int[] re = buffer.highlightStart();
                    int col = searchBackwards ? buffer.length(buffer.getLine(re[0])) - re[1] : re[1];
                    int match = re[0]*10000 + col;
                    if (matches.contains(match)) {
                        break;
                    } else {
                        matches.add(match);
                    }
                    if (!all) {
                        op = getYNC("Replace this instance? ", true);
                    }
                } else {
                    op = Operation.NO;
                }
                switch (op) {
                case ALL:
                    all = true;
                    buffer.replaceFromCursor(matchedLength, replaceTerm);
                    replaced++;
                    break;
                case YES:
                    buffer.replaceFromCursor(matchedLength, replaceTerm);
                    replaced++;
                    break;
                case NO:
                    break;
                case CANCEL:
                    found = false;
                    break;
                default:
                    break;
                }
            }
            message = "Replaced " + replaced + " occurrences";
        } catch (Exception e) {
            // ignore
        } finally {
            searchToReplace = false;
            matchedLength =  -1;
            this.shortcuts = standardShortcuts();
            editMessage = null;
        }
    }

    void search() throws IOException {
        KeyMap searchKeyMap = new KeyMap<>();
        searchKeyMap.setUnicode(Operation.INSERT);
//        searchKeyMap.setNomatch(Operation.INSERT);
        for (char i = 32; i < 256; i++) {
            searchKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        for (char i = 'A'; i <= 'Z'; i++) {
            searchKeyMap.bind(Operation.DO_LOWER_CASE, alt(i));
        }
        searchKeyMap.bind(Operation.BACKSPACE, del());
        searchKeyMap.bind(Operation.CASE_SENSITIVE, alt('c'));
        searchKeyMap.bind(Operation.BACKWARDS, alt('b'));
        searchKeyMap.bind(Operation.REGEXP, alt('r'));
        searchKeyMap.bind(Operation.ACCEPT, "\r");
        searchKeyMap.bind(Operation.CANCEL, ctrl('C'));
        searchKeyMap.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1));
        searchKeyMap.bind(Operation.FIRST_LINE, ctrl('Y'));
        searchKeyMap.bind(Operation.LAST_LINE, ctrl('V'));
        searchKeyMap.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse));
        searchKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right));
        searchKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left));
        searchKeyMap.bind(Operation.UP, key(terminal, Capability.key_up));
        searchKeyMap.bind(Operation.DOWN, key(terminal, Capability.key_down));
        searchKeyMap.bind(Operation.TOGGLE_REPLACE, ctrl('R'));

        editMessage = getSearchMessage();
        editBuffer.setLength(0);
        String currentBuffer = editBuffer.toString();
        int curPos = editBuffer.length();
        this.shortcuts = searchShortcuts();
        display(curPos);
        try {
            while (true) {
                Operation op = readOperation(searchKeyMap);
                switch (op) {
                    case UP:
                        editBuffer.setLength(0);
                        editBuffer.append(patternHistory.up(currentBuffer));
                        curPos = editBuffer.length();
                        break;
                    case DOWN:
                        editBuffer.setLength(0);
                        editBuffer.append(patternHistory.down(currentBuffer));
                        curPos = editBuffer.length();
                        break;
                    case CASE_SENSITIVE:
                        searchCaseSensitive = !searchCaseSensitive;
                        break;
                    case BACKWARDS:
                        searchBackwards = !searchBackwards;
                        break;
                    case REGEXP:
                        searchRegexp = !searchRegexp;
                        break;
                    case CANCEL:
                        throw new IllegalArgumentException();
                    case ACCEPT:
                        if (editBuffer.length() > 0) {
                            searchTerm = editBuffer.toString();
                        }
                        if (searchTerm == null || searchTerm.isEmpty()) {
                            setMessage("Cancelled");
                            throw new IllegalArgumentException();
                        } else {
                            patternHistory.add(searchTerm);
                            if (!searchToReplace) {
                                buffer.nextSearch();
                            }
                        }
                        return;
                    case HELP:
                        if (searchToReplace) {
                            help("nano-search-replace-help.txt");
                        } else {
                            help("nano-search-help.txt");
                        }
                        break;
                    case FIRST_LINE:
                        buffer.firstLine();
                        break;
                    case LAST_LINE:
                        buffer.lastLine();
                        break;
                    case MOUSE_EVENT:
                        mouseEvent();
                        break;
                    case TOGGLE_REPLACE:
                        searchToReplace = !searchToReplace;
                        this.shortcuts = searchShortcuts();
                        break;
                    default:
                        curPos = editInputBuffer(op, curPos);
                        currentBuffer = editBuffer.toString();
                        break;
               }
                editMessage = getSearchMessage();
                display(curPos);
            }
        } finally {
            this.shortcuts = standardShortcuts();
            editMessage = null;
        }
    }

    String replace() throws IOException {
        KeyMap keyMap = new KeyMap<>();
        keyMap.setUnicode(Operation.INSERT);
//        keyMap.setNomatch(Operation.INSERT);
        for (char i = 32; i < 256; i++) {
            keyMap.bind(Operation.INSERT, Character.toString(i));
        }
        for (char i = 'A'; i <= 'Z'; i++) {
            keyMap.bind(Operation.DO_LOWER_CASE, alt(i));
        }
        keyMap.bind(Operation.BACKSPACE, del());
        keyMap.bind(Operation.ACCEPT, "\r");
        keyMap.bind(Operation.CANCEL, ctrl('C'));
        keyMap.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1));
        keyMap.bind(Operation.FIRST_LINE, ctrl('Y'));
        keyMap.bind(Operation.LAST_LINE, ctrl('V'));
        keyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right));
        keyMap.bind(Operation.LEFT, key(terminal, Capability.key_left));
        keyMap.bind(Operation.UP, key(terminal, Capability.key_up));
        keyMap.bind(Operation.DOWN, key(terminal, Capability.key_down));

        editMessage = "Replace with: ";
        editBuffer.setLength(0);
        String currentBuffer = editBuffer.toString();
        int curPos = editBuffer.length();
        this.shortcuts = replaceShortcuts();
        display(curPos);
        try {
            while (true) {
                Operation op = readOperation(keyMap);
                switch (op) {
                    case UP:
                        editBuffer.setLength(0);
                        editBuffer.append(patternHistory.up(currentBuffer));
                        curPos = editBuffer.length();
                        break;
                    case DOWN:
                        editBuffer.setLength(0);
                        editBuffer.append(patternHistory.down(currentBuffer));
                        curPos = editBuffer.length();
                        break;
                    case CANCEL:
                        throw new IllegalArgumentException();
                    case ACCEPT:
                        String replaceTerm = "";
                        if (editBuffer.length() > 0) {
                            replaceTerm = editBuffer.toString();
                        }
                        if (replaceTerm == null) {
                            setMessage("Cancelled");
                            throw new IllegalArgumentException();
                        } else {
                            patternHistory.add(replaceTerm);
                        }
                        return replaceTerm;
                    case HELP:
                        help("nano-replace-help.txt");
                        break;
                    case FIRST_LINE:
                        buffer.firstLine();
                        break;
                    case LAST_LINE:
                        buffer.lastLine();
                        break;
                    case MOUSE_EVENT:
                        mouseEvent();
                        break;
                    default:
                        curPos = editInputBuffer(op, curPos);
                        currentBuffer = editBuffer.toString();
                        break;
                }
                display(curPos);
            }
        } finally {
            this.shortcuts = standardShortcuts();
            editMessage = null;
        }
    }

    private String getSearchMessage() {
        StringBuilder sb = new StringBuilder();
        sb.append("Search");
        if (searchToReplace) {
            sb.append(" (to replace)");
        }
        if (searchCaseSensitive) {
            sb.append(" [Case Sensitive]");
        }
        if (searchRegexp) {
            sb.append(" [Regexp]");
        }
        if (searchBackwards) {
            sb.append(" [Backwards]");
        }
        if (searchTerm != null) {
            sb.append(" [");
            sb.append(searchTerm);
            sb.append("]");
        }
        sb.append(": ");
        return sb.toString();
    }

    String computeCurPos() {
        int chari = 0;
        int chart = 0;
        for (int i = 0; i < buffer.lines.size(); i++) {
            int l = buffer.lines.get(i).length() + 1;
            if (i < buffer.line) {
                chari += l;
            } else if (i == buffer.line) {
                chari += buffer.offsetInLine + buffer.column;
            }
            chart += l;
        }

        StringBuilder sb = new StringBuilder();
        sb.append("line ");
        sb.append(buffer.line + 1);
        sb.append("/");
        sb.append(buffer.lines.size());
        sb.append(" (");
        sb.append(Math.round((100.0 * buffer.line) / buffer.lines.size()));
        sb.append("%), ");
        sb.append("col ");
        sb.append(buffer.offsetInLine + buffer.column + 1);
        sb.append("/");
        sb.append(buffer.length(buffer.lines.get(buffer.line)) + 1);
        sb.append(" (");
        if (buffer.lines.get(buffer.line).length() > 0) {
            sb.append(Math.round((100.0 * (buffer.offsetInLine + buffer.column))
                    / (buffer.length(buffer.lines.get(buffer.line)))));
        } else {
            sb.append("100");
        }
        sb.append("%), ");
        sb.append("char ");
        sb.append(chari + 1);
        sb.append("/");
        sb.append(chart);
        sb.append(" (");
        sb.append(Math.round((100.0 * chari) / chart));
        sb.append("%)");
        return sb.toString();
    }

    void curPos() {
        setMessage(computeCurPos());
    }

    void prevBuffer() throws IOException {
        if (buffers.size() > 1) {
            bufferIndex = (bufferIndex + buffers.size() - 1) % buffers.size();
            buffer = buffers.get(bufferIndex);
            setMessage("Switched to " + buffer.getTitle());
            buffer.open();
            display.clear();
        } else {
            setMessage("No more open file buffers");
        }
    }

    void nextBuffer() throws IOException {
        if (buffers.size() > 1) {
            bufferIndex = (bufferIndex + 1) % buffers.size();
            buffer = buffers.get(bufferIndex);
            setMessage("Switched to " + buffer.getTitle());
            buffer.open();
            display.clear();
        } else {
            setMessage("No more open file buffers");
        }
    }

    void setMessage(String message) {
        this.message = message;
        this.nbBindings = quickBlank ? 2 : 25;
    }

    boolean quit() throws IOException {
        if (buffer.dirty) {
            if (tempFile) {
                if (!write()) {
                    return false;
                }
            } else {
                Operation op = getYNC("Save modified buffer (ANSWERING \"No\" WILL DESTROY CHANGES) ? ");
                switch (op) {
                    case CANCEL:
                        return false;
                    case NO:
                        break;
                    case YES:
                        if (!write()) {
                            return false;
                        }
                }
            }
        }
        buffers.remove(bufferIndex);
        if (bufferIndex == buffers.size() && bufferIndex > 0) {
            bufferIndex = buffers.size() - 1;
        }
        if (buffers.isEmpty()) {
            buffer = null;
            return true;
        } else {
            buffer = buffers.get(bufferIndex);
            buffer.open();
            display.clear();
            setMessage("Switched to " + buffer.getTitle());
            return false;
        }
    }

    void numbers() {
        printLineNumbers = !printLineNumbers;
        resetDisplay();
        setMessage("Lines numbering " + (printLineNumbers ? "enabled" : "disabled"));
    }

    void smoothScrolling() {
        smoothScrolling = !smoothScrolling;
        setMessage("Smooth scrolling " + (smoothScrolling ? "enabled" : "disabled"));
    }

    void mouseSupport() throws IOException {
        mouseSupport = !mouseSupport;
        setMessage("Mouse support " + (mouseSupport ? "enabled" : "disabled"));
        terminal.trackMouse(mouseSupport ? Terminal.MouseTracking.Normal : Terminal.MouseTracking.Off);
    }

    void constantCursor() {
        constantCursor = !constantCursor;
        setMessage("Constant cursor position display " + (constantCursor ? "enabled" : "disabled"));
    }

    void oneMoreLine() {
        oneMoreLine = !oneMoreLine;
        setMessage("Use of one more line for editing " + (oneMoreLine ? "enabled" : "disabled"));
    }

    void wrap() {
        wrapping = !wrapping;
        buffer.computeAllOffsets();
        resetDisplay();
        setMessage("Lines wrapping " + (wrapping ? "enabled" : "disabled"));
    }

    void clearScreen() {
        resetDisplay();
    }

    void mouseEvent() {
        MouseEvent event = terminal.readMouseEvent();
        if (event.getModifiers().isEmpty() && event.getType() == MouseEvent.Type.Released
                && event.getButton() == MouseEvent.Button.Button1) {
            int x = event.getX();
            int y = event.getY();
            int hdr = buffer.computeHeader().size();
            int ftr = computeFooter().size();
            if (y < hdr) {
                // nothing
            } else if (y < size.getRows() - ftr) {
                buffer.moveTo(x, y - hdr);
            } else {
                int cols = (shortcuts.size() + 1) / 2;
                int cw = size.getColumns() / cols;
                int l = y - (size.getRows() - ftr) - 1;
                int si = l * cols +  x / cw;
                String shortcut = null;
                Iterator it = shortcuts.keySet().iterator();
                while (si-- >= 0 && it.hasNext()) { shortcut = it.next(); }
                if (shortcut != null) {
                    shortcut = shortcut.replaceAll("M-", "\\\\E");
                    String seq = KeyMap.translate(shortcut);
                    bindingReader.runMacro(seq);
                }
            }
        }
        else if (event.getType() == MouseEvent.Type.Wheel) {
            if (event.getButton() == MouseEvent.Button.WheelDown) {
                buffer.moveDown(1);
            } else if (event.getButton() == MouseEvent.Button.WheelUp) {
                buffer.moveUp(1);
            }
        }
    }

    void enableSuspension() {
        if (!restricted && vsusp < 0) {
            Attributes attrs = terminal.getAttributes();
            attrs.setControlChar(ControlChar.VSUSP, vsusp);
            terminal.setAttributes(attrs);
        }
    }

    void toggleSuspension() {
        if (restricted) {
            setMessage("This function is disabled in restricted mode");
        } else if (vsusp < 0) {
            setMessage("This function is disabled");
        } else {
            Attributes attrs = terminal.getAttributes();
            int toggle = vsusp;
            String message = "enabled";
            if (attrs.getControlChar(ControlChar.VSUSP) > 0) {
                toggle = 0;
                message = "disabled";
            }
            attrs.setControlChar(ControlChar.VSUSP, toggle);
            terminal.setAttributes(attrs);
            setMessage("Suspension " + message);
        }
    }

    public String getTitle() {
        return title;
    }

    void resetDisplay() {
        display.clear();
        display.resize(size.getRows(), size.getColumns());
        for (Buffer buffer : buffers) {
            buffer.resetDisplay();
        }
    }

    synchronized void display() {
        display(null);
    }

    synchronized void display(final Integer editCursor) {
        if (nbBindings > 0) {
            if (--nbBindings == 0) {
                message = null;
            }
        }

        List header = buffer.computeHeader();
        List footer = computeFooter();

        int nbLines = size.getRows() - header.size() - footer.size();
        List newLines = buffer.getDisplayedLines(nbLines);
        newLines.addAll(0, header);
        newLines.addAll(footer);

        // Compute cursor position
        int cursor;
        if (editMessage != null) {
            int crsr = editCursor != null ? editCursor : editBuffer.length();
            cursor = editMessage.length() + crsr;
            cursor = size.cursorPos(size.getRows() - footer.size(), cursor);
        } else {
            cursor = size.cursorPos(header.size(),
                                    buffer.getDisplayedCursor());
        }
        display.update(newLines, cursor);
        if (windowsTerminal) {
            resetDisplay();
        }
    }

    protected List computeFooter() {
        List footer = new ArrayList<>();

        if (editMessage != null) {
            AttributedStringBuilder sb = new AttributedStringBuilder();
            sb.style(AttributedStyle.INVERSE);
            sb.append(editMessage);
            sb.append(editBuffer);
            for (int i = editMessage.length() + editBuffer.length(); i < size.getColumns(); i++) {
                sb.append(' ');
            }
            sb.append('\n');
            footer.add(sb.toAttributedString());
        } else if (message!= null || constantCursor) {
            int rwidth = size.getColumns();
            String text = "[ " + (message == null ? computeCurPos() : message) + " ]";
            int len = text.length();
            AttributedStringBuilder sb = new AttributedStringBuilder();
            for (int i = 0; i < (rwidth - len) / 2; i++) {
                sb.append(' ');
            }
            sb.style(AttributedStyle.INVERSE);
            sb.append(text);
            sb.append('\n');
            footer.add(sb.toAttributedString());
        } else {
            footer.add(new AttributedString("\n"));
        }

        Iterator> sit = shortcuts.entrySet().iterator();
        int cols = (shortcuts.size() + 1) / 2;
        int cw = (size.getColumns() - 1) / cols;
        int rem = (size.getColumns() - 1) % cols;
        for (int l = 0; l < 2; l++) {
            AttributedStringBuilder sb = new AttributedStringBuilder();
            for (int c = 0; c < cols; c++) {
                Map.Entry entry = sit.hasNext() ? sit.next() : null;
                String key = entry != null ? entry.getKey() : "";
                String val = entry != null ? entry.getValue() : "";
                sb.style(AttributedStyle.INVERSE);
                sb.append(key);
                sb.style(AttributedStyle.DEFAULT);
                sb.append(" ");
                int nb = cw - key.length() - 1 + (c < rem ? 1 : 0);
                if (val.length() > nb) {
                    sb.append(val.substring(0, nb));
                } else {
                    sb.append(val);
                    if (c < cols - 1) {
                        for (int i = 0; i < nb - val.length(); i++) {
                            sb.append(" ");
                        }
                    }
                }
            }
            sb.append('\n');
            footer.add(sb.toAttributedString());
        }

        return footer;
    }

    protected void handle(Signal signal) {
        if (buffer != null) {
            size.copy(terminal.getSize());
            buffer.computeAllOffsets();
            buffer.moveToChar(buffer.offsetInLine + buffer.column);
            resetDisplay();
            display();
        }
    }

    protected void bindKeys() {
        keys = new KeyMap<>();
        if (!view) {
            keys.setUnicode(Operation.INSERT);

            for (char i = 32; i < KEYMAP_LENGTH; i++) {
                keys.bind(Operation.INSERT, Character.toString(i));
            }
            keys.bind(Operation.BACKSPACE, del());
            for (char i = 'A'; i <= 'Z'; i++) {
                keys.bind(Operation.DO_LOWER_CASE, alt(i));
            }
            keys.bind(Operation.WRITE, ctrl('O'), key(terminal, Capability.key_f3));
            keys.bind(Operation.JUSTIFY_PARAGRAPH, ctrl('J'), key(terminal, Capability.key_f4));
            keys.bind(Operation.CUT, ctrl('K'), key(terminal, Capability.key_f9));
            keys.bind(Operation.UNCUT, ctrl('U'), key(terminal, Capability.key_f10));
            keys.bind(Operation.REPLACE, ctrl('\\'), key(terminal, Capability.key_f14), alt('r'));
            keys.bind(Operation.MARK, ctrl('^'), key(terminal, Capability.key_f15), alt('a'));
            keys.bind(Operation.COPY, alt('^'), alt('6'));
            keys.bind(Operation.INDENT, alt('}'));
            keys.bind(Operation.UNINDENT, alt('{'));
            keys.bind(Operation.VERBATIM, alt('v'));
            keys.bind(Operation.INSERT, ctrl('I'), ctrl('M'));
            keys.bind(Operation.DELETE, ctrl('D'), key(terminal, Capability.key_dc));
            keys.bind(Operation.BACKSPACE, ctrl('H'));
            keys.bind(Operation.CUT_TO_END, alt('t'));
            keys.bind(Operation.JUSTIFY_FILE, alt('j'));
            keys.bind(Operation.AUTO_INDENT, alt('i'));
            keys.bind(Operation.CUT_TO_END_TOGGLE, alt('k'));
            keys.bind(Operation.TABS_TO_SPACE, alt('q'));
        } else {
            keys.bind(Operation.NEXT_PAGE, " ", "f");
            keys.bind(Operation.PREV_PAGE, "b");
        }
        keys.bind(Operation.NEXT_PAGE, ctrl('V'), key(terminal, Capability.key_f8));
        keys.bind(Operation.PREV_PAGE, ctrl('Y'), key(terminal, Capability.key_f7));

        keys.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1));
        keys.bind(Operation.QUIT, ctrl('X'), key(terminal, Capability.key_f2));

        keys.bind(Operation.READ, ctrl('R'), key(terminal, Capability.key_f5));
        keys.bind(Operation.SEARCH, ctrl('W'), key(terminal, Capability.key_f6));

        keys.bind(Operation.CUR_POS, ctrl('C'), key(terminal, Capability.key_f11));
        keys.bind(Operation.TO_SPELL, ctrl('T'), key(terminal, Capability.key_f11));

        keys.bind(Operation.GOTO, ctrl('_'), key(terminal, Capability.key_f13), alt('g'));
        keys.bind(Operation.NEXT_SEARCH, key(terminal, Capability.key_f16), alt('w'));

        keys.bind(Operation.RIGHT, ctrl('F'));
        keys.bind(Operation.LEFT, ctrl('B'));
        keys.bind(Operation.NEXT_WORD, ctrl(' '));
        keys.bind(Operation.PREV_WORD, alt(' '));
        keys.bind(Operation.UP, ctrl('P'));
        keys.bind(Operation.DOWN, ctrl('N'));

        keys.bind(Operation.BEGINNING_OF_LINE, ctrl('A'), key(terminal, Capability.key_home));
        keys.bind(Operation.END_OF_LINE, ctrl('E'), key(terminal, Capability.key_end));
        keys.bind(Operation.BEGINNING_OF_PARAGRAPH, alt('('), alt('9'));
        keys.bind(Operation.END_OF_PARAGRAPH, alt(')'), alt('0'));
        keys.bind(Operation.FIRST_LINE, alt('\\'), alt('|'));
        keys.bind(Operation.LAST_LINE, alt('/'), alt('?'));

        keys.bind(Operation.MATCHING, alt(']'));
        keys.bind(Operation.SCROLL_UP, alt('-'), alt('_'));
        keys.bind(Operation.SCROLL_DOWN, alt('+'), alt('='));

        keys.bind(Operation.PREV_BUFFER, alt('<'));
        keys.bind(Operation.NEXT_BUFFER, alt('>'));
        keys.bind(Operation.PREV_BUFFER, alt(','));
        keys.bind(Operation.NEXT_BUFFER, alt('.'));

        keys.bind(Operation.COUNT, alt('d'));
        keys.bind(Operation.CLEAR_SCREEN, ctrl('L'));

        keys.bind(Operation.HELP, alt('x'));
        keys.bind(Operation.CONSTANT_CURSOR, alt('c'));
        keys.bind(Operation.ONE_MORE_LINE, alt('o'));
        keys.bind(Operation.SMOOTH_SCROLLING, alt('s'));
        keys.bind(Operation.MOUSE_SUPPORT, alt('m'));
        keys.bind(Operation.WHITESPACE, alt('p'));
        keys.bind(Operation.HIGHLIGHT, alt('y'));

        keys.bind(Operation.SMART_HOME_KEY, alt('h'));
        keys.bind(Operation.WRAP, alt('l'));

        keys.bind(Operation.BACKUP, alt('b'));
        keys.bind(Operation.NUMBERS, alt('n'));

        keys.bind(Operation.UP, key(terminal, Capability.key_up));
        keys.bind(Operation.DOWN, key(terminal, Capability.key_down));
        keys.bind(Operation.RIGHT, key(terminal, Capability.key_right));
        keys.bind(Operation.LEFT, key(terminal, Capability.key_left));
        keys.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse));
        keys.bind(Operation.TOGGLE_SUSPENSION, alt('z'));
        keys.bind(Operation.NEXT_PAGE, key(terminal, Capability.key_npage));
        keys.bind(Operation.PREV_PAGE, key(terminal, Capability.key_ppage));
    }

    protected enum Operation {
        DO_LOWER_CASE,

        QUIT,
        WRITE,
        READ,
        GOTO,
        FIND,

        WRAP,
        NUMBERS,
        SMOOTH_SCROLLING,
        MOUSE_SUPPORT,
        ONE_MORE_LINE,
        CLEAR_SCREEN,

        UP,
        DOWN,
        LEFT,
        RIGHT,

        INSERT,
        BACKSPACE,

        NEXT_BUFFER,
        PREV_BUFFER,

        HELP,
        NEXT_PAGE,
        PREV_PAGE,
        SCROLL_UP,
        SCROLL_DOWN,
        NEXT_WORD,
        PREV_WORD,
        BEGINNING_OF_LINE,
        END_OF_LINE,
        FIRST_LINE,
        LAST_LINE,

        CUR_POS,

        CASE_SENSITIVE,
        BACKWARDS,
        REGEXP,
        ACCEPT,
        CANCEL,
        SEARCH,
        TOGGLE_REPLACE,
        MAC_FORMAT,
        DOS_FORMAT,
        APPEND_MODE,
        PREPEND_MODE,
        BACKUP,
        TO_FILES,
        YES,
        NO,
        ALL,
        NEW_BUFFER,
        EXECUTE,
        NEXT_SEARCH,
        MATCHING,
        VERBATIM,
        DELETE,

        JUSTIFY_PARAGRAPH,
        TO_SPELL,
        CUT,
        REPLACE,
        MARK,
        COPY,
        INDENT,
        UNINDENT,
        BEGINNING_OF_PARAGRAPH,
        END_OF_PARAGRAPH,
        CUT_TO_END,
        JUSTIFY_FILE,
        COUNT,
        CONSTANT_CURSOR,
        WHITESPACE,
        HIGHLIGHT,
        SMART_HOME_KEY,
        AUTO_INDENT,
        CUT_TO_END_TOGGLE,
        TABS_TO_SPACE,
        UNCUT,

        MOUSE_EVENT,

        TOGGLE_SUSPENSION
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy