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